cvprac 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ # BSD 3-Clause License
4
+ #
5
+ # Copyright (c) 2017, Arista Networks EOS+
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # * Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ #
14
+ # * Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # * Neither the name Arista nor the names of its
19
+ # contributors may be used to endorse or promote products derived from
20
+ # this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+
34
+ # @author Arista EOS+ Consulting Services <eosplus-dev@arista.com>
35
+ module Cvprac
36
+ # Cvprac::Api namespace
37
+ module Api
38
+ # CVP Info api methods
39
+ module Info
40
+ # rubocop:disable Style/AccessorMethodName
41
+ # @!group Info Method Summary
42
+
43
+ # Get CVP version information
44
+ #
45
+ # @return [Hash] CVP Version data. Ex: {"version"=>"2016.1.1"}
46
+ def get_cvp_info
47
+ @clnt.get('/cvpInfo/getCvpInfo.do')
48
+ # return @clnt.get('/cvpInfo/getCvpInfo.do',
49
+ # timeout: @request_timeout)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ # BSD 3-Clause License
4
+ #
5
+ # Copyright (c) 2017, Arista Networks EOS+
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # * Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ #
14
+ # * Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # * Neither the name Arista nor the names of its
19
+ # contributors may be used to endorse or promote products derived from
20
+ # this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+
34
+ # @author Arista EOS+ Consulting Services <eosplus-dev@arista.com>
35
+ module Cvprac
36
+ # Cvprac::Api namespace
37
+ module Api
38
+ # CVP Inventory api methods
39
+ module Inventory
40
+ # @!group Inventory Method Summary
41
+
42
+ # Get device (NetElement) by name (fqdn)
43
+ #
44
+ # @param [String] fqdn The FQDN (name) of the desired device
45
+ #
46
+ # @return [Hash] CVP NetElement data.
47
+ def get_device_by_name(fqdn)
48
+ log(Logger::DEBUG) { "get_device_by_name: #{fqdn}" }
49
+ res = @clnt.get('/inventory/getInventory.do',
50
+ data: { queryparam: fqdn,
51
+ startIndex: 0,
52
+ endIndex: 0 })
53
+ return {} if res['netElementList'].length.zero?
54
+ res['netElementList'].each do |element|
55
+ return element if element['fqdn'] == fqdn
56
+ end
57
+ {}
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: utf-8
2
+
3
+ # BSD 3-Clause License
4
+ #
5
+ # Copyright (c) 2017, Arista Networks EOS+
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # * Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ #
14
+ # * Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # * Neither the name Arista nor the names of its
19
+ # contributors may be used to endorse or promote products derived from
20
+ # this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+
34
+ # @author Arista EOS+ Consulting Services <eosplus-dev@arista.com>
35
+ module Cvprac
36
+ # Cvprac::Api namespace
37
+ module Api
38
+ # CVP provisioning api methods
39
+ module Provisioning
40
+ # @!group Provisioning Method Summary
41
+
42
+ # Get configlets by device ID
43
+ #
44
+ # @param [String] sys_mac The netElementId (System MAC) of the device
45
+ # @param opts [Hash] Optional arguments
46
+ # @option opts [String] :queryparam Search string
47
+ # @option opts [Fixnum] :start_index (0) Start index for pagination
48
+ # @option opts [Fixnum] :end_index (0) End index for pagination
49
+ #
50
+ # @return [Array] List of configlets applied to the device
51
+ # rubocop:disable Metrics/MethodLength
52
+ def get_configlets_by_device_id(sys_mac, **opts)
53
+ opts = { queryparam: nil,
54
+ start_index: 0,
55
+ end_index: 0 }.merge(opts)
56
+ log(Logger::DEBUG) do
57
+ "get_configlets_by_device_id: #{sys_mac} with query: #{opts.inspect}"
58
+ end
59
+ res = @clnt.get('/provisioning/getConfigletsByNetElementId.do',
60
+ data: { netElementId: sys_mac,
61
+ queryParam: opts[:queryparam],
62
+ startIndex: opts[:start_index],
63
+ endIndex: opts[:end_index] })
64
+ res['configletList']
65
+ end
66
+ # rubocop:enable Metrics/MethodLength
67
+
68
+ private
69
+
70
+ # Add a temp action. Requires a save_topology_v2() call to take effect.
71
+ #
72
+ # @param [Hash] data the data object to process
73
+ # Ex: data = {'data': [{specific key/value pairs}]}
74
+ #
75
+ # @return [Hash] A Topology hash
76
+ # rubocop:disable Metrics/MethodLength
77
+ def add_temp_action(data)
78
+ log(Logger::DEBUG) do
79
+ "#{__method__}: #{data.inspect}"
80
+ end
81
+ resp = @clnt.post('/provisioning/addTempAction.do',
82
+ data: { format: 'topology',
83
+ queryParam: nil,
84
+ nodeId: 'root' },
85
+ body: data)
86
+ log(Logger::DEBUG) do
87
+ "#{__method__}: response #{resp.inspect}"
88
+ end
89
+ resp
90
+ end
91
+ # rubocop:enable Metrics/MethodLength
92
+
93
+ # Commits a temp action. See #add_temp_action
94
+ #
95
+ # @param [Array] data a list that contains a dict with a specific
96
+ # format for the desired action. Our primary use case is for
97
+ # confirming existing temp actions so we most often send an
98
+ # empty list to confirm an existing temp action.
99
+ # Example:
100
+ #
101
+ # @return [Hash] Contains a status and a list of task ids created,
102
+ # if any.
103
+ #
104
+ # @example
105
+ # => {u'data': {u'status': u'success', u'taskIds': []}}
106
+ def save_topology_v2(data)
107
+ log(Logger::DEBUG) do
108
+ "#{__method__}: #{data.inspect}"
109
+ end
110
+ resp = @clnt.post('/provisioning/v2/saveTopology.do',
111
+ body: data)
112
+ log(Logger::DEBUG) do
113
+ "#{__method__}: response #{resp.inspect}"
114
+ end
115
+ resp
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,108 @@
1
+ # encoding: utf-8
2
+
3
+ # BSD 3-Clause License
4
+ #
5
+ # Copyright (c) 2017, Arista Networks EOS+
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # * Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ #
14
+ # * Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # * Neither the name Arista nor the names of its
19
+ # contributors may be used to endorse or promote products derived from
20
+ # this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+
34
+ # @author Arista EOS+ Consulting Services <eosplus-dev@arista.com>
35
+ module Cvprac
36
+ # CvpRac::Api namespace
37
+ module Api
38
+ # CVP Task api methods
39
+ module Task
40
+ # @!group Task Method Summary
41
+
42
+ # Get task data by ID
43
+ #
44
+ # @param [String] task_id The id of the task to execute
45
+ #
46
+ # @return [Hash] request body
47
+ def get_task_by_id(task_id)
48
+ log(Logger::DEBUG) { "#{__method__}: task_id: #{task_id}" }
49
+ begin
50
+ task = @clnt.get('/task/getTaskById.do', data: { taskId: task_id })
51
+ rescue CvpApiError => e
52
+ if e.to_s.include?('Invalid WorkOrderId') ||
53
+ e.to_s.include?('Entity does not exist')
54
+ return nil
55
+ end
56
+ end
57
+ task
58
+ end
59
+
60
+ # Get task data by device name (FQDN)
61
+ #
62
+ # @param [String] device Name (FQDN) of a device
63
+ #
64
+ # @return [Hash] request body
65
+ # rubocop:disable Metrics/MethodLength
66
+ def get_pending_tasks_by_device(device)
67
+ log(Logger::DEBUG) { "#{__method__}: device: #{device}" }
68
+ begin
69
+ task = @clnt.get('/task/getTasks.do', data: { queryparam: 'Pending',
70
+ startIndex: 0,
71
+ endIndex: 0 })
72
+ rescue CvpApiError => e
73
+ if e.to_s.include?('Invalid WorkOrderId') ||
74
+ e.to_s.include?('Entity does not exist')
75
+ return nil
76
+ end
77
+ end
78
+ # TODO: filter tasks by device
79
+ task['data']
80
+ end
81
+ # rubocop:enable Metrics/MethodLength
82
+
83
+ # Add note to CVP task by taskID
84
+ #
85
+ # @param [String] task_id The id of the task to execute
86
+ # @param [String] note Content of the note
87
+ #
88
+ # @return [Hash] request body
89
+ def add_note_to_task(task_id, note)
90
+ log(Logger::DEBUG) do
91
+ "add_note_to_task: task_id: #{task_id}, note: [#{note}]"
92
+ end
93
+ @clnt.post('/task/addNoteToTask.do',
94
+ data: { workOrderId: task_id, note: note })
95
+ end
96
+
97
+ # Execute CVP task by taskID
98
+ #
99
+ # @param [String] task_id The id of the task to execute
100
+ #
101
+ # @return [Hash] request body
102
+ def execute_task(task_id)
103
+ log(Logger::DEBUG) { "execute_task: task_id: #{task_id}" }
104
+ @clnt.post('/task/executeTask.do', body: { data: [task_id] })
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,560 @@
1
+ # encoding: utf-8
2
+
3
+ # BSD 3-Clause License
4
+ #
5
+ # Copyright (c) 2016, Arista Networks EOS+
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # * Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ #
14
+ # * Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # * Neither the name Arista nor the names of its
19
+ # contributors may be used to endorse or promote products derived from
20
+ # this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
+ #
33
+ # rubocop:disable Metrics/ClassLength
34
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
35
+
36
+ require 'cgi'
37
+ require 'http-cookie'
38
+ require 'json'
39
+ require 'logger'
40
+ require 'net/http'
41
+ require 'pp'
42
+ require 'syslog/logger'
43
+
44
+ # Provide simplified RESTful methods to access Arista CloudVision Portal
45
+ #
46
+ # Establish and maintain connections with Arista CloudVision Portal servers,
47
+ # providing basic RESTful methods which handle session, cookie, and reconnects
48
+ # behind the scenes.
49
+ #
50
+ # @example Basic usage
51
+ # require 'cvprac'
52
+ # cvp = CvpClient.new
53
+ # cvp.connect(['cvp1', 'cvp2', 'cvp3'], 'cvpadmin', 'arista123')
54
+ # result = cvp.get('/user/getUsers.do',
55
+ # data: {queryparam: nil,
56
+ # startIndex: 0,
57
+ # endIndex: 0})
58
+ # pp(result)
59
+ # {"total"=>1,
60
+ # "users"=>
61
+ # [{"userId"=>"cvpadmin",
62
+ # "firstName"=>nil,
63
+ # "email"=>"nobody@example.com",
64
+ # "lastAccessed"=>1483726955950,
65
+ # "userStatus"=>"Enabled",
66
+ # "currentStatus"=>"Online",
67
+ # "contactNumber"=>nil,
68
+ # "factoryId"=>1,
69
+ # "lastName"=>nil,
70
+ # "password"=>nil,
71
+ # "id"=>28}],
72
+ # "roles"=>{"cvpadmin"=>["network-admin"]}}
73
+ #
74
+ # cvp.post('/test/endpoint.do', body: '{"some":"data"}')
75
+ #
76
+ # @author Arista EOS+ Consulting Services <eosplus-dev@arista.com>
77
+ class CvpClient
78
+ METHOD_LIST = {
79
+ get: Net::HTTP::Get,
80
+ post: Net::HTTP::Post,
81
+ put: Net::HTTP::Put,
82
+ head: Net::HTTP::Head,
83
+ delete: Net::HTTP::Delete
84
+ }.freeze
85
+ private_constant :METHOD_LIST
86
+
87
+ # Maximum number of times to retry a get or post to the same
88
+ # CVP node.
89
+ NUM_RETRY_REQUESTS = 3
90
+
91
+ # @!attribute [rw] agent
92
+ # Agent is the first part of the complete User-Agent
93
+ # @example User-Agent
94
+ # "User-agent"=>"cvp_app (x86_64-darwin14) cvprac-rb/0.1.0"
95
+ # @return [String] Application name included in HTTP User-Agent passed to
96
+ # CloudVision Portal. (Default: $PROGRAM_NAME) The full User-Agent string
97
+ # includes the application name, system-OS, and cvprac version
98
+ # information.
99
+ # @!attribute [rw] connect_timeout
100
+ # @return [Fixnum] Max number of seconds before failing an HTTP connect
101
+ # @!attribute [rw] headers
102
+ # @return [Hash] HTTP request headers
103
+ # @!attribute [rw] port
104
+ # @return [Fixnum] TCP port used for connections
105
+ # @!attribute [rw] protocol
106
+ # @return [String] 'http' or 'https'
107
+ # @!attribute [rw] ssl_verify_mode
108
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER
109
+ # @see http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL.html#module-OpenSSL-label-Peer+Verification
110
+ # @!attribute [rw] logger.level
111
+ # logger severity level: Logger::DEBUG < Logger::INFO < Logger::WARN <
112
+ # Logger::ERROR < Logger::FATAL. This allows the user to increase or
113
+ # decrease the logging level of the STDOUT log as needed throughout their
114
+ # application.
115
+ # @!attribute [rw] api
116
+ # An instance of CvpApi
117
+ attr_accessor :agent, :connect_timeout, :headers,
118
+ :port, :protocol, :ssl_verify_mode, :file_log_level, :api
119
+
120
+ # @!attribute [r] cookies
121
+ # @return [HTTP::CookieJar] HTTP cookies sent with each authenticated
122
+ # request
123
+ # @!attribute [r] headers
124
+ # @return [Hash] HTTP headers sent with each request
125
+ # @!attribute [r] nodes
126
+ # @return [Array<String>] List of configured CloudVision Portal nodes
127
+ attr_reader :cookies, :headers, :nodes
128
+
129
+ def file_log_level=(value)
130
+ @file_log_level = value
131
+ # Update existing handles if they exist
132
+ @logstdout.level = @file_log_level if @logstdout.level
133
+ @logfile.level = @file_log_level if @logfile.level
134
+ end
135
+
136
+ # Initialize a new CvpClient object
137
+ #
138
+ # @param opts [Hash] Optional arguments
139
+ # @option opts [String] :logger ('cvprac') Logging name for this service
140
+ # @option opts [Bool] :syslog (false) Log to the syslog service?
141
+ # @option opts [String] :filename (nil) A local logfile to use, if provided
142
+ # @option opts [Logger::level] :file_log_level (Logger::INFO) The default
143
+ # logging level which will be recorded in the logs. See the Logging
144
+ # rubygem for additional severity levels
145
+ def initialize(**opts)
146
+ opts = { logger: 'cvprac',
147
+ syslog: false,
148
+ filename: nil,
149
+ file_log_level: Logger::INFO }.merge(opts)
150
+ @agent = File.basename($PROGRAM_NAME)
151
+ @agent_full = "#{@agent} (#{RUBY_PLATFORM}) "\
152
+ "cvprac-rb/#{Cvprac::VERSION}"
153
+ @authdata = nil
154
+ @connect_timeout = nil
155
+ @cookies = HTTP::CookieJar.new
156
+ @error_msg = nil
157
+ @file_log_level = opts[:file_log_level]
158
+ @headers = { 'Accept' => 'application/json',
159
+ 'Content-Type' => 'application/json',
160
+ 'User-agent' => @agent_full }
161
+ @node_count = nil
162
+ @node_pool = nil
163
+ @nodes = nil
164
+ @port = nil
165
+ @protocol = nil
166
+ @session = nil
167
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER
168
+ @ssl_verify_mode = OpenSSL::SSL::VERIFY_NONE
169
+ @url_prefix = nil
170
+
171
+ if opts[:filename] == 'STDOUT'
172
+ @logstdout = Logger.new(STDOUT)
173
+ @logstdout.level = @file_log_level
174
+ else
175
+ unless opts[:filename].nil?
176
+ @logfile = Logger.new(opts[:filename])
177
+ @logfile.level = @file_log_level
178
+ end
179
+ end
180
+ @syslog = Syslog::Logger.new(opts[:filename]) if opts[:syslog]
181
+
182
+ # Instantiate the CvpApi class
183
+ @api = CvpApi.new(self)
184
+
185
+ log(Logger::INFO, 'CvpClient initialized')
186
+ end
187
+
188
+ # Log message to all configured loggers
189
+ #
190
+ # @overload log(severity: Logger::INFO, msg: nil)
191
+ # @param severity [Logger] Severity to log to:
192
+ # DEBUG < INFO < WARN < ERROR < FATAL
193
+ # @param msg [String] Message to log
194
+ #
195
+ # @overload log(severity: Logger::INFO)
196
+ # @param severity [Logger] Severity to log to:
197
+ # DEBUG < INFO < WARN < ERROR < FATAL
198
+ # @yield [msg] Messages can be passed as a block to delay evaluation
199
+ def log(severity = Logger::INFO, msg = nil)
200
+ msg = yield if block_given?
201
+ @logstdout.add(severity, msg) if defined? @logstdout
202
+ @logfile.add(severity, msg) if defined? @logfile
203
+ @syslog.add(severity, msg) if defined? @syslog
204
+ end
205
+
206
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
207
+
208
+ # Connect to one or more CVP nodes.
209
+ #
210
+ # @param nodes [Array] Hostnames or IPs of the CVP node or nodes
211
+ # @param username [String] CVP username
212
+ # @param password [String] CVP password
213
+ #
214
+ # @param opts [Hash] Optional arguments
215
+ # @option opts [Fixnum] :connect_timeout (10) Seconds to wait before failing
216
+ # a connect. Default: 10
217
+ # @option opts [String] :protocol ('https') 'http' or 'https' to use when
218
+ # connecting to the CVP. Default: https
219
+ # @option opts [Fixnum] :port (nil) TCP port to which we should connect is
220
+ # not standard http/https port.
221
+ # @option opts [Bool] :verify_ssl (false) Verify CVP SSL certificate?
222
+ # Requires that a valid (non-self-signed) certificate be installed on the
223
+ # CloudVision Portal node(s).
224
+ def connect(nodes, username, password, **opts)
225
+ opts = { connect_timeout: 10,
226
+ protocol: 'https',
227
+ port: nil,
228
+ verify_ssl: false }.merge(opts)
229
+ connect_timeout = opts[:connect_timeout]
230
+ protocol = opts[:protocol]
231
+ port = opts[:port]
232
+
233
+ @nodes = Array(nodes) # Ensure nodes is always an array
234
+ @node_index = 0
235
+ @node_count = nodes.length
236
+ @node_last = @node_count - 1
237
+ @node_pool = Enumerator.new do |y|
238
+ loop do
239
+ index = @node_index % @node_count
240
+ if @node_index == @node_last
241
+ @node_index = 0
242
+ else
243
+ @node_index += 1
244
+ end
245
+ y.yield @nodes[index]
246
+ end
247
+ end
248
+ @authdata = { userId: username, password: password }
249
+ @connect_timeout = connect_timeout
250
+ @protocol = protocol
251
+
252
+ if port.nil?
253
+ if protocol == 'http'
254
+ port = 80
255
+ elsif protocol == 'https'
256
+ port = 443
257
+ else
258
+ raise ArgumentError, "No default port for protocol: #{protocol}"
259
+ end
260
+ end
261
+ @port = port
262
+
263
+ @ssl_verify_mode = if opts[:verify_ssl]
264
+ OpenSSL::SSL::VERIFY_PEER
265
+ else
266
+ OpenSSL::SSL::VERIFY_NONE
267
+ end
268
+
269
+ create_session(nil)
270
+ raise CvpLoginError, @error_msg unless @session
271
+ end
272
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
273
+
274
+ # @!group RESTful methods
275
+
276
+ # Send an HTTP GET request with session data and return the response.
277
+ #
278
+ # @param endpoint [String] URL endpoint starting after `https://host:port/web`
279
+ #
280
+ # @param [Hash] opts Optional parameters
281
+ # @option opts [Hash] :data (nil) query parameters
282
+ #
283
+ # @return [JSON] parsed response body
284
+ def get(endpoint, **opts)
285
+ data = opts.key?(:data) ? opts[:data] : nil
286
+ make_request(:get, endpoint, data: data)
287
+ end
288
+
289
+ # Send an HTTP POST request with session data and return the response.
290
+ #
291
+ # @param endpoint [String] URL endpoint starting after `https://host:port/web`
292
+ #
293
+ # @param [Hash] opts Optional parameters
294
+ # @option opts [JSON] :body (nil) JSON body to post
295
+ # @option opts [Hash] :data (nil) query parameters
296
+ # @return [Net::HTTP Response]
297
+ def post(endpoint, **opts)
298
+ data = opts.key?(:data) ? opts[:data] : nil
299
+ body = opts.key?(:body) ? opts[:body] : nil
300
+ make_request(:post, endpoint, data: data, body: body)
301
+ end
302
+
303
+ # @!endgroup RESTful methods
304
+
305
+ private
306
+
307
+ # Send an HTTP request with session data and return the response.
308
+ #
309
+ # @param method [Symbol] Reuqest method: :get, :post, :head, etc.
310
+ # @param endpoint [String] URI path to the endpoint after /web/
311
+ #
312
+ # @param opts [Hash] Optional arguments
313
+ # @option opts [JSON] :body JSON body to post
314
+ # @option opts [Hash] :data query parameters
315
+ # @option opts [Fixnum] :timeout (30) Seconds to timeout request.
316
+ #
317
+ # @return [JSON] parsed response body
318
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
319
+ def make_request(method, endpoint, **opts)
320
+ opts = { data: nil, body: nil, timeout: 30 }.merge(opts)
321
+ log(Logger::DEBUG) do
322
+ "entering make_request #{method} "\
323
+ "endpoint: #{endpoint}"\
324
+ " with query: #{opts[:data].inspect}" \
325
+ " with body: #{opts[:body].inspect}"
326
+ end
327
+ raise 'No valid session to a CVP node. Use #connect()' unless @session
328
+
329
+ # Ensure body is valid JSON
330
+ if opts[:body]
331
+ case opts[:body]
332
+ when String
333
+ JSON.parse(opts[:body])
334
+ when Hash, Array
335
+ opts[:body] = opts[:body].to_json
336
+ else
337
+ raise ArgumentError, "Unable to coerce body to JSON: #{opts[:body]}"
338
+ end
339
+ end
340
+
341
+ url = @url_prefix + endpoint
342
+ uri = URI(url)
343
+ uri.query = URI.encode_www_form(opts[:data]) if opts[:data]
344
+ http = Net::HTTP.new(uri.host, uri.port)
345
+ http.read_timeout = opts[:timeout]
346
+ if @protocol == 'https'
347
+ http.use_ssl = true
348
+ http.verify_mode = @ssl_verify_mode
349
+ end
350
+
351
+ error = nil
352
+ retry_count = NUM_RETRY_REQUESTS
353
+ node_count = @node_count
354
+ while node_count > 0
355
+ unless error.nil?
356
+ log(Logger::DEBUG) { "make_request: error not nil: #{error}" }
357
+ node_count -= 1
358
+ raise error if node_count.zero?
359
+ create_session
360
+
361
+ raise error unless @session
362
+ retry_count = NUM_RETRY_REQUESTS
363
+ error = nil
364
+ end
365
+
366
+ begin
367
+ log(Logger::DEBUG) { 'make_request: ' + uri.request_uri }
368
+ request = METHOD_LIST[method].new(uri.request_uri, @headers)
369
+ request.body = opts[:body] if opts[:body]
370
+ response = http.request(request)
371
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
372
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
373
+ Net::ProtocolError => error
374
+ log(Logger::ERROR) { "Request failed: #{error}" }
375
+ raise CvpRequestError, error
376
+ rescue => error
377
+ log(Logger::ERROR) { "UnknownError: #{error}" }
378
+ raise error
379
+ end
380
+ log(Logger::DEBUG) { 'Request succeeded. Checking response...' }
381
+
382
+ begin
383
+ good_response?(response, "#{method} #{uri.request_uri}:")
384
+ rescue CvpSessionLogOutError => error
385
+ log(Logger::DEBUG) { "Session logged out: #{error}" }
386
+ retry_count -= 1
387
+ if retry_count > 0
388
+ log(Logger::DEBUG) do
389
+ 'Session logged out... resetting and retrying '\
390
+ "#{error}"
391
+ end
392
+ reset_session
393
+ error = nil if @session # rubocop:disable Metrics/BlockNesting
394
+ else
395
+ msg = 'Session logged out. Failed to re-login. '\
396
+ "No more retries: #{error}"
397
+ log(Logger::ERROR) { msg }
398
+ raise CvpSessionLogOutError, msg
399
+ end
400
+ next
401
+ end
402
+ log(Logger::DEBUG) { 'make_request completed.' }
403
+ break
404
+ end
405
+
406
+ response.body ? JSON.parse(response.body) : nil
407
+ end
408
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
409
+
410
+ # Login to CVP and get a session ID and user information.
411
+ # If the all_nodes parameter is True then try creating a session
412
+ # with each CVP node. If False, then try creating a session with
413
+ # each node except the one currently connected to.
414
+ #
415
+ # @param all_nodes [Bool] Establish a session with each node or just one
416
+ def create_session(all_nodes = nil)
417
+ node_count = @node_count
418
+ node_count -= 1 if all_nodes.nil? && node_count > 1
419
+
420
+ @error_msg = '\n'
421
+ (0...node_count).each do
422
+ host = @node_pool.next
423
+ @url_prefix = "#{@protocol}://#{host}:#{@port}/web"
424
+ @http = Net::HTTP.new(host, @port)
425
+ if @protocol == 'https'
426
+ @http.use_ssl = true
427
+ @http.verify_mode = @ssl_verify_mode
428
+ end
429
+ error = reset_session
430
+ break if error.nil?
431
+ @error_msg += "#{host}: #{error}\n"
432
+ end
433
+ end
434
+
435
+ # Get a new request session and try logging into the current
436
+ # CVP node. If the login succeeded None will be returned and
437
+ # @session will be valid. If the login failed then an
438
+ # exception error will be returned and @session will
439
+ # be set to None.
440
+ #
441
+ # @return [String] nil on success or errors encountered
442
+ def reset_session
443
+ @session = nil
444
+ error = nil
445
+
446
+ begin
447
+ login
448
+ rescue CvpApiError, CvpRequestError, CvpSessionLogOutError => error
449
+ log(Logger::ERROR) { error }
450
+ # Invalidate the session due to error
451
+ @session = nil
452
+ end
453
+ error
454
+ end
455
+
456
+ # Check the response from Net::HTTP
457
+ # If the response is not good data, generate a useful log message, then
458
+ # raise an appropriate exception.
459
+ #
460
+ # @param response [Net::HTTP response object]
461
+ # @param prefix [String] Optional text to prepend to error messages
462
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
463
+ def good_response?(response, prefix = '')
464
+ log(Logger::DEBUG) { "response_code: #{response.code}" }
465
+ log(Logger::DEBUG) { 'response_headers: ' + response.to_hash.to_s }
466
+ log(Logger::DEBUG) { "response_body: #{response.body}" }
467
+ if response.respond_to?('reason')
468
+ log(Logger::DEBUG) { "response_reason: #{response.reason}" }
469
+ end
470
+
471
+ if response.code.to_i == 302
472
+ msg = "#{prefix} Notice302: session logged out"
473
+ log(Logger::DEBUG) { msg }
474
+ raise CvpSessionLogOutError, msg
475
+ elsif response.code.to_i != 200
476
+ msg = "#{prefix}: Request Error"
477
+ if response.code.to_i == 400
478
+ title = response.body.match(%r{<h1>(.*?)</h1>})[1]
479
+ msg = "#{prefix}: #{title}" if title
480
+ end
481
+ log(Logger::ERROR) { 'ErrorCode: ' + response.code + ' - ' + msg }
482
+ msg += " Reason: #{response.reason}" if response.respond_to?('reason')
483
+ raise CvpRequestError.new(response.code, msg)
484
+ end
485
+
486
+ log(Logger::DEBUG) { 'Got a response 200 with a body' }
487
+ return unless response.body.to_s.include? 'errorCode'
488
+
489
+ log(Logger::DEBUG) { 'Body has an errorCode' }
490
+ body = JSON.parse(response.body)
491
+ if body['errorCode'] == 'MNF404'
492
+ msg = 'Invalid endpoint'
493
+ raise CvpRequestError.new('HTTP Status 404', msg)
494
+ end
495
+ if body.key?('errorMessage')
496
+ err_msg = "errorCode: #{body['errorCode']}: #{body['errorMessage']}"
497
+ log(Logger::ERROR) { err_msg }
498
+ else
499
+ error_list = if body.key?('errors')
500
+ body['errors']
501
+ else
502
+ [body['errorCode']]
503
+ end
504
+ err_msg = error_list[0]
505
+ (1...error_list.length).each do |idx|
506
+ err_msg += "\n#{error_list[idx]}"
507
+ end
508
+ end
509
+
510
+ msg = "#{prefix}: Request Error: #{err_msg}"
511
+ log(Logger::ERROR) { msg }
512
+ raise CvpApiError, msg
513
+ end
514
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
515
+
516
+ # Make a POST request to CVP login authentication.
517
+ # An error can be raised from the post method call or the
518
+ # good_response? method call. Any errors raised would be a good
519
+ # reason not to use this host.
520
+ #
521
+ # @raise SomeError
522
+ def login
523
+ @headers.delete('APP_SESSION_ID')
524
+ url = @url_prefix + '/login/authenticate.do'
525
+ uri = URI(url)
526
+ http = Net::HTTP.new(uri.host, uri.port)
527
+ if @protocol == 'https'
528
+ http.use_ssl = true
529
+ http.verify_mode = @ssl_verify_mode
530
+ end
531
+
532
+ request = Net::HTTP::Post.new(uri.path, @headers)
533
+ request.body = @authdata.to_json
534
+ log(Logger::DEBUG) { 'Sending login POST' }
535
+ begin
536
+ response = http.request(request)
537
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
538
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
539
+ Net::ProtocolError => error
540
+ log(Logger::ERROR) { 'Login failed: ' + error.to_s }
541
+ raise CvpLoginError, error.to_s
542
+ rescue => error
543
+ log(Logger::ERROR) { 'Login failed UnkReason: ' + error.to_s }
544
+ raise CvpLoginError, error.to_s
545
+ end
546
+ log(Logger::DEBUG) { 'Sent login POST' }
547
+
548
+ good_response?(response, 'Authenticate:')
549
+ log(Logger::DEBUG) { 'login checked response' }
550
+
551
+ response.get_fields('Set-Cookie').each do |value|
552
+ @cookies.parse(value, @url_prefix)
553
+ end
554
+
555
+ body = JSON.parse(response.body)
556
+ @session = @headers['APP_SESSION_ID'] = body['sessionId']
557
+ @headers['Cookie'] = HTTP::Cookie.cookie_value(@cookies.cookies)
558
+ log(Logger::DEBUG) { 'login SUCCESS' }
559
+ end
560
+ end