cvprac 1.0.0

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.
@@ -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