shopify-junos-ez-stdlib 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +91 -0
- data/LICENSE +26 -0
- data/README.md +199 -0
- data/docs/Facts.md +192 -0
- data/docs/Providers/Group.md +61 -0
- data/docs/Providers/IPports.md +61 -0
- data/docs/Providers/L1ports.md +29 -0
- data/docs/Providers/L2ports.md +43 -0
- data/docs/Providers/LAGports.md +57 -0
- data/docs/Providers/StaticHosts.md +26 -0
- data/docs/Providers/StaticRoutes.md +37 -0
- data/docs/Providers/UserAuths.md +32 -0
- data/docs/Providers/Users.md +122 -0
- data/docs/Providers/Vlans.md +43 -0
- data/docs/Providers_Resources.md +353 -0
- data/docs/README_FIRST.md +27 -0
- data/docs/Utils/Config.md +160 -0
- data/docs/Utils/Filesystem.md +360 -0
- data/docs/Utils/Routing-Engine.md +379 -0
- data/docs/Utils/SCP.md +24 -0
- data/examples/config/config_file.rb +72 -0
- data/examples/config/config_template_object.rb +81 -0
- data/examples/config/config_template_simple.rb +76 -0
- data/examples/config/multi_config.rb +60 -0
- data/examples/fs_utils.rb +31 -0
- data/examples/lag_port.rb +27 -0
- data/examples/re_upgrade.rb +99 -0
- data/examples/re_utils.rb +33 -0
- data/examples/simple.rb +46 -0
- data/examples/st_hosts.rb +33 -0
- data/examples/user.rb +32 -0
- data/examples/vlans.rb +31 -0
- data/junos-ez-stdlib.gemspec +15 -0
- data/lib/junos-ez/exceptions.rb +3 -0
- data/lib/junos-ez/facts.rb +83 -0
- data/lib/junos-ez/facts/chassis.rb +51 -0
- data/lib/junos-ez/facts/ifd_style.rb +17 -0
- data/lib/junos-ez/facts/personality.rb +25 -0
- data/lib/junos-ez/facts/switch_style.rb +31 -0
- data/lib/junos-ez/facts/version.rb +58 -0
- data/lib/junos-ez/group.rb +206 -0
- data/lib/junos-ez/ip_ports.rb +30 -0
- data/lib/junos-ez/ip_ports/classic.rb +188 -0
- data/lib/junos-ez/l1_ports.rb +121 -0
- data/lib/junos-ez/l1_ports/classic.rb +87 -0
- data/lib/junos-ez/l1_ports/switch.rb +134 -0
- data/lib/junos-ez/l2_ports.rb +66 -0
- data/lib/junos-ez/l2_ports/bridge_domain.rb +499 -0
- data/lib/junos-ez/l2_ports/vlan.rb +433 -0
- data/lib/junos-ez/l2_ports/vlan_l2ng.rb +502 -0
- data/lib/junos-ez/lag_ports.rb +268 -0
- data/lib/junos-ez/provider.rb +619 -0
- data/lib/junos-ez/stdlib.rb +18 -0
- data/lib/junos-ez/system.rb +48 -0
- data/lib/junos-ez/system/st_hosts.rb +92 -0
- data/lib/junos-ez/system/st_routes.rb +159 -0
- data/lib/junos-ez/system/syscfg.rb +103 -0
- data/lib/junos-ez/system/userauths.rb +84 -0
- data/lib/junos-ez/system/users.rb +217 -0
- data/lib/junos-ez/utils/config.rb +236 -0
- data/lib/junos-ez/utils/fs.rb +385 -0
- data/lib/junos-ez/utils/re.rb +558 -0
- data/lib/junos-ez/version.rb +6 -0
- data/lib/junos-ez/vlans.rb +38 -0
- data/lib/junos-ez/vlans/bridge_domain.rb +89 -0
- data/lib/junos-ez/vlans/vlan.rb +119 -0
- data/lib/junos-ez/vlans/vlan_l2ng.rb +126 -0
- data/shipit.yml +4 -0
- data/tmp +7 -0
- metadata +129 -0
@@ -0,0 +1,217 @@
|
|
1
|
+
=begin
|
2
|
+
=end
|
3
|
+
|
4
|
+
require 'junos-ez/system/userauths'
|
5
|
+
|
6
|
+
module Junos::Ez::Users
|
7
|
+
|
8
|
+
PROPERTIES = [
|
9
|
+
:uid, # User-ID, Number
|
10
|
+
:class, # User Class, String
|
11
|
+
:fullname, # Full Name, String
|
12
|
+
:password, # Encrypted password
|
13
|
+
:ssh_keys, # READ-ONLY, Hash of SSH public keys
|
14
|
+
]
|
15
|
+
|
16
|
+
def self.Provider( ndev, varsym )
|
17
|
+
newbie = Junos::Ez::Users::Provider.new( ndev )
|
18
|
+
newbie.properties = Junos::Ez::Provider::PROPERTIES + PROPERTIES
|
19
|
+
Junos::Ez::Provider.attach_instance_variable( ndev, varsym, newbie )
|
20
|
+
end
|
21
|
+
|
22
|
+
class Provider < Junos::Ez::Provider::Parent; end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
##### ---------------------------------------------------------------
|
28
|
+
##### Provider Resource Methods
|
29
|
+
##### ---------------------------------------------------------------
|
30
|
+
|
31
|
+
class Junos::Ez::Users::Provider
|
32
|
+
|
33
|
+
### ---------------------------------------------------------------
|
34
|
+
### XML top placement
|
35
|
+
### ---------------------------------------------------------------
|
36
|
+
|
37
|
+
def xml_at_top
|
38
|
+
Nokogiri::XML::Builder.new{|x| x.configuration{
|
39
|
+
x.system { x.login { x.user {
|
40
|
+
x.name @name
|
41
|
+
return x
|
42
|
+
}}}}}
|
43
|
+
end
|
44
|
+
|
45
|
+
### ---------------------------------------------------------------
|
46
|
+
### XML readers
|
47
|
+
### ---------------------------------------------------------------
|
48
|
+
|
49
|
+
def xml_get_has_xml( xml )
|
50
|
+
xml.xpath('//user')[0]
|
51
|
+
end
|
52
|
+
|
53
|
+
def xml_read_parser( as_xml, as_hash )
|
54
|
+
set_has_status( as_xml, as_hash )
|
55
|
+
|
56
|
+
as_hash[:uid] = as_xml.xpath('uid').text
|
57
|
+
as_hash[:class] = as_xml.xpath('class').text
|
58
|
+
|
59
|
+
xml_when_item(as_xml.xpath('full-name')) {|i|
|
60
|
+
as_hash[:fullname] = i.text
|
61
|
+
}
|
62
|
+
|
63
|
+
xml_when_item(as_xml.xpath('authentication/encrypted-password')) {|i|
|
64
|
+
as_hash[:password] = i.text
|
65
|
+
}
|
66
|
+
|
67
|
+
# READ-ONLY capture the keys
|
68
|
+
unless (keys = as_xml.xpath('authentication/ssh-rsa')).empty?
|
69
|
+
as_hash[:ssh_keys] ||= {}
|
70
|
+
as_hash[:ssh_keys]['ssh-rsa'] = keys.collect{|key| key.text.strip}
|
71
|
+
end
|
72
|
+
unless (keys = as_xml.xpath('authentication/ssh-dsa')).empty?
|
73
|
+
as_hash[:ssh_keys] ||= {}
|
74
|
+
as_hash[:ssh_keys]['ssh-dsa'] = keys.collect{|key| key.text.strip}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
### ---------------------------------------------------------------
|
79
|
+
### XML writers
|
80
|
+
### ---------------------------------------------------------------
|
81
|
+
|
82
|
+
def xml_change_password( xml )
|
83
|
+
xml.authentication {
|
84
|
+
xml_set_or_delete( xml, 'encrypted-password', @should[:password] )
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def xml_change_fullname( xml )
|
89
|
+
xml_set_or_delete( xml, 'full-name', @should[:fullname] )
|
90
|
+
end
|
91
|
+
|
92
|
+
# changing the 'gid' is changing the Junos 'class' element
|
93
|
+
# so, what is tough here is that the Nokogiri Builder mech
|
94
|
+
# won't allow us to use the string 'class' since it conflicts
|
95
|
+
# with the Ruby language. So we need to add the 'class' element
|
96
|
+
# the hard way, yo! ...
|
97
|
+
|
98
|
+
def xml_change_class( xml )
|
99
|
+
par = xml.instance_variable_get(:@parent)
|
100
|
+
doc = xml.instance_variable_get(:@doc)
|
101
|
+
user_class = Nokogiri::XML::Node.new('class', doc )
|
102
|
+
user_class.content = @should[:class]
|
103
|
+
par.add_child( user_class )
|
104
|
+
end
|
105
|
+
|
106
|
+
def xml_change_uid( xml )
|
107
|
+
xml_set_or_delete( xml, 'uid', @should[:uid] )
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
##### ---------------------------------------------------------------
|
113
|
+
##### Provider Collection Methods
|
114
|
+
##### ---------------------------------------------------------------
|
115
|
+
|
116
|
+
class Junos::Ez::Users::Provider
|
117
|
+
|
118
|
+
def build_list
|
119
|
+
@ndev.rpc.get_configuration{ |x| x.system {
|
120
|
+
x.login {
|
121
|
+
x.user({:recurse => 'false'})
|
122
|
+
}
|
123
|
+
}}
|
124
|
+
.xpath('//user/name').collect{ |i| i.text }
|
125
|
+
end
|
126
|
+
|
127
|
+
def build_catalog
|
128
|
+
@catalog = {}
|
129
|
+
@ndev.rpc.get_configuration{ |x| x.system {
|
130
|
+
x.login
|
131
|
+
}}
|
132
|
+
.xpath('//user').each do |user|
|
133
|
+
name = user.xpath('name').text
|
134
|
+
@catalog[name] = {}
|
135
|
+
xml_read_parser( user, @catalog[name] )
|
136
|
+
end
|
137
|
+
@catalog
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
##### ---------------------------------------------------------------
|
143
|
+
##### Resource Methods
|
144
|
+
##### ---------------------------------------------------------------
|
145
|
+
|
146
|
+
class Junos::Ez::Users::Provider
|
147
|
+
|
148
|
+
## ----------------------------------------------------------------
|
149
|
+
## change the password by providing it in plain-text
|
150
|
+
## ----------------------------------------------------------------
|
151
|
+
|
152
|
+
def password=(plain_text)
|
153
|
+
xml = xml_at_top
|
154
|
+
xml.authentication {
|
155
|
+
xml.send(:'plain-text-password-value', plain_text)
|
156
|
+
}
|
157
|
+
@ndev.rpc.load_configuration( xml )
|
158
|
+
return true
|
159
|
+
end
|
160
|
+
|
161
|
+
## ----------------------------------------------------------------
|
162
|
+
## get a Hash that is used as the 'name' for obtaining a resource
|
163
|
+
## for Junos::Ez::UserAuths::Provider
|
164
|
+
## ----------------------------------------------------------------
|
165
|
+
|
166
|
+
def ssh_key( keytype, index = 0 )
|
167
|
+
return nil unless @has[:ssh_keys]
|
168
|
+
return nil unless @has[:ssh_keys][keytype]
|
169
|
+
ret_h = {:user => @name, :keytype => keytype}
|
170
|
+
ret_h[:publickey] = @has[:ssh_keys][keytype][index]
|
171
|
+
ret_h
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
## @@ need to move this code into the main provider
|
176
|
+
## @@ as a utility ...
|
177
|
+
##
|
178
|
+
|
179
|
+
def get_userauth_provd
|
180
|
+
@ndev.providers.each do |p|
|
181
|
+
obj = @ndev.send(p)
|
182
|
+
return obj if obj.class == Junos::Ez::UserAuths::Provider
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
## ----------------------------------------------------------------
|
187
|
+
## load an SSH public key & return the resulting key object.
|
188
|
+
## You can provide the publickey either as :publickey or
|
189
|
+
## contents will be read from :filename
|
190
|
+
## ----------------------------------------------------------------
|
191
|
+
|
192
|
+
def load_ssh_key!( opts = {} )
|
193
|
+
publickey = opts[:publickey] || File.read( opts[:filename] ).strip
|
194
|
+
raise ArgumentError, "no public-key specified" unless publickey
|
195
|
+
|
196
|
+
# nab the provider for handling ssh-keys, since we'll use that
|
197
|
+
# for key resource management
|
198
|
+
|
199
|
+
@auth_provd ||= get_userauth_provd
|
200
|
+
raise StandardError, "No Junos::Ez::UserAuths::Provider" unless @auth_provd
|
201
|
+
|
202
|
+
# extract the key-type from the public key.
|
203
|
+
keytype = publickey[0..6]
|
204
|
+
keytype = 'ssh-dsa' if keytype == 'ssh-dss'
|
205
|
+
raise ArgumentError, "Unknown ssh key-type #{keytype}" unless Junos::Ez::UserAuths::VALID_KEY_TYPES.include? keytype
|
206
|
+
|
207
|
+
# ok, we've got everything we need to add the key, so here we go.
|
208
|
+
key_name = {:user => @name, :keytype => keytype, :publickey => publickey }
|
209
|
+
key = @auth_provd[ key_name ]
|
210
|
+
key.write!
|
211
|
+
|
212
|
+
# return the key in case the caller wants it
|
213
|
+
key
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
@@ -0,0 +1,236 @@
|
|
1
|
+
=begin
|
2
|
+
---------------------------------------------------------------------
|
3
|
+
Config::Utils is a collection of methods used for loading
|
4
|
+
configuration files/templates and software images
|
5
|
+
|
6
|
+
commit! - commit configuration
|
7
|
+
commit? - see if a candidate config is OK (commit-check)
|
8
|
+
diff? - shows the diff of the candidate config w/current | rolback
|
9
|
+
load! - load configuration onto device
|
10
|
+
lock! - take exclusive lock on config
|
11
|
+
unlock! - release exclusive lock on config
|
12
|
+
rollback! - perform a config rollback
|
13
|
+
get_config - returns requested config in "text" format-style
|
14
|
+
|
15
|
+
---------------------------------------------------------------------
|
16
|
+
=end
|
17
|
+
|
18
|
+
module Junos::Ez::Config
|
19
|
+
def self.Utils( ndev, varsym )
|
20
|
+
newbie = Junos::Ez::Config::Provider.new( ndev )
|
21
|
+
Junos::Ez::Provider.attach_instance_variable( ndev, varsym, newbie )
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
### -----------------------------------------------------------------
|
26
|
+
### PUBLIC METHODS
|
27
|
+
### -----------------------------------------------------------------
|
28
|
+
### -----------------------------------------------------------------
|
29
|
+
|
30
|
+
class Junos::Ez::Config::Provider < Junos::Ez::Provider::Parent
|
31
|
+
|
32
|
+
### ---------------------------------------------------------------
|
33
|
+
### load! - used to load configuration files / templates. This
|
34
|
+
### does not perform a 'commit', just the equivalent of the
|
35
|
+
### load-configuration RPC
|
36
|
+
###
|
37
|
+
### --- options ---
|
38
|
+
###
|
39
|
+
### :filename => path - indcates the filename of content
|
40
|
+
### note: filename extension will also define format
|
41
|
+
### .{conf,text,txt} <==> :text
|
42
|
+
### .xml <==> :xml
|
43
|
+
### .set <==> :set
|
44
|
+
###
|
45
|
+
### :content => String - string content of data (vs. :filename)
|
46
|
+
###
|
47
|
+
### :format => [:text, :set, :xml], default :text (curly-brace)
|
48
|
+
### this will override any auto-format from the :filename
|
49
|
+
###
|
50
|
+
### :binding - indicates file/content is an ERB
|
51
|
+
### => <object> - will grab the binding from this object
|
52
|
+
### using a bit of meta-programming magic
|
53
|
+
### => <binding> - will use this binding
|
54
|
+
###
|
55
|
+
### :replace! => true - enables the 'replace' option
|
56
|
+
### :overwrite! => true - enables the 'overwrite' optoin
|
57
|
+
###
|
58
|
+
### --- returns ---
|
59
|
+
### true if the configuration is loaded OK
|
60
|
+
### raise Netconf::EditError otherwise
|
61
|
+
### ---------------------------------------------------------------
|
62
|
+
|
63
|
+
def load!( opts = {} )
|
64
|
+
raise ArgumentError unless opts[:content] || opts[:filename]
|
65
|
+
|
66
|
+
content = opts[:content] || File.read( opts[:filename] )
|
67
|
+
|
68
|
+
attrs = {}
|
69
|
+
attrs[:action] = 'replace' if opts[:replace!]
|
70
|
+
attrs[:action] = 'override' if opts[:override!]
|
71
|
+
|
72
|
+
if opts[:format]
|
73
|
+
attrs[:format] = opts[:format].to_s
|
74
|
+
elsif opts[:filename]
|
75
|
+
case f_ext = File.extname( opts[:filename] )
|
76
|
+
when '.conf','.text','.txt'; attrs[:format] = 'text'
|
77
|
+
when '.set'; attrs[:format] = 'set'
|
78
|
+
when '.xml'; # default is XML
|
79
|
+
else
|
80
|
+
raise ArgumentError, "unknown format from extension: #{f_ext}"
|
81
|
+
end
|
82
|
+
else
|
83
|
+
raise ArgumentError "unspecified format"
|
84
|
+
end
|
85
|
+
|
86
|
+
if opts[:binding]
|
87
|
+
erb = ERB.new( content, nil, '>' )
|
88
|
+
case opts[:binding]
|
89
|
+
when Binding
|
90
|
+
# binding was provided to use
|
91
|
+
content = erb.result( opts[:binding] )
|
92
|
+
when Object
|
93
|
+
obj = opts[:binding]
|
94
|
+
def obj.junos_ez_binding; binding end
|
95
|
+
content = erb.result( obj.junos_ez_binding )
|
96
|
+
class << obj; remove_method :junos_ez_binding end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
@ndev.rpc.load_configuration( content, attrs )
|
101
|
+
true # everthing OK!
|
102
|
+
end
|
103
|
+
|
104
|
+
### ---------------------------------------------------------------
|
105
|
+
### commit! - commits the configuration to the device
|
106
|
+
###
|
107
|
+
### --- options ---
|
108
|
+
###
|
109
|
+
### :confirm => true | timeout
|
110
|
+
### :comment => commit log comment
|
111
|
+
###
|
112
|
+
### --- returns ---
|
113
|
+
### true if commit completed
|
114
|
+
### raises Netconf::CommitError otherwise
|
115
|
+
### ---------------------------------------------------------------
|
116
|
+
|
117
|
+
def commit!( opts = {} )
|
118
|
+
|
119
|
+
args = {}
|
120
|
+
args[:log] = opts[:comment] if opts[:comment]
|
121
|
+
if opts[:confirm]
|
122
|
+
args[:confirmed] = true
|
123
|
+
if opts[:confirm] != true
|
124
|
+
timeout = Integer( opts[:confirm] ) rescue false
|
125
|
+
raise ArgumentError "invalid timeout #{opts[:confirm]}" unless timeout
|
126
|
+
args[:confirm_timeout] = timeout
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
@ndev.rpc.commit_configuration( args )
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
### ---------------------------------------------------------------
|
135
|
+
### commit? - perform commit configuration check
|
136
|
+
###
|
137
|
+
### --- returns ---
|
138
|
+
### true if candidate config is OK to commit
|
139
|
+
### Array of rpc-error data otherwise
|
140
|
+
### ---------------------------------------------------------------
|
141
|
+
|
142
|
+
def commit?
|
143
|
+
begin
|
144
|
+
@ndev.rpc.commit_configuration( :check => true )
|
145
|
+
rescue => e
|
146
|
+
return Junos::Ez::rpc_errors( e.rsp )
|
147
|
+
end
|
148
|
+
true # commit check OK!
|
149
|
+
end
|
150
|
+
|
151
|
+
### ---------------------------------------------------------------
|
152
|
+
### rollback! - used to rollback the configuration
|
153
|
+
### ---------------------------------------------------------------
|
154
|
+
|
155
|
+
def rollback!( rollback_id = 0 )
|
156
|
+
raise ArgumentError, "invalid rollback #{rollback_id}" unless ( rollback_id >= 0 and rollback_id <= 50 )
|
157
|
+
@ndev.rpc.load_configuration( :compare=>'rollback', :rollback=> rollback_id.to_s )
|
158
|
+
true # rollback OK!
|
159
|
+
end
|
160
|
+
|
161
|
+
### ---------------------------------------------------------------
|
162
|
+
### diff? - displays diff (patch format) between
|
163
|
+
### current candidate configuration loaded and the rollback_id
|
164
|
+
###
|
165
|
+
### --- returns ---
|
166
|
+
### nil if no diff
|
167
|
+
### String of diff output otherwise
|
168
|
+
### ---------------------------------------------------------------
|
169
|
+
|
170
|
+
def diff?( rollback_id = 0 )
|
171
|
+
raise ArgumentError, "invalid rollback #{rollback_id}" unless ( rollback_id >= 0 and rollback_id <= 50 )
|
172
|
+
got = ndev.rpc.get_configuration( :compare=>'rollback', :rollback=> rollback_id.to_s )
|
173
|
+
diff = got.xpath('configuration-output').text
|
174
|
+
return nil if diff == "\n"
|
175
|
+
diff
|
176
|
+
end
|
177
|
+
|
178
|
+
### ---------------------------------------------------------------
|
179
|
+
### lock! - takes an exclusive lock on the candidate config
|
180
|
+
###
|
181
|
+
### --- returns ---
|
182
|
+
### true if lock acquired
|
183
|
+
### raise Netconf::LockError otherwise
|
184
|
+
### ---------------------------------------------------------------
|
185
|
+
|
186
|
+
def lock!
|
187
|
+
@ndev.rpc.lock_configuration
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
### ---------------------------------------------------------------
|
192
|
+
### unlock! - releases exclusive lock on candidate config
|
193
|
+
###
|
194
|
+
### --- returns ---
|
195
|
+
### true if lock release
|
196
|
+
### raise Netconf::RpcError otherwise
|
197
|
+
### ---------------------------------------------------------------
|
198
|
+
|
199
|
+
def unlock!
|
200
|
+
@ndev.rpc.unlock_configuration
|
201
|
+
true
|
202
|
+
end
|
203
|
+
|
204
|
+
### ---------------------------------------------------------------
|
205
|
+
### get_config - returns String of requested (or entire) config
|
206
|
+
### in "text" (curly-brace) format. The 'rqst' argument
|
207
|
+
### identifies the scope of the config, for example:
|
208
|
+
###
|
209
|
+
### .get_config( "interfaces ge-0/0/0" )
|
210
|
+
###
|
211
|
+
### If there is no configuration available, 'nil' is returned
|
212
|
+
###
|
213
|
+
### If there is an error in the request, that will be returned
|
214
|
+
### as a String with "ERROR!" prepended
|
215
|
+
### ---------------------------------------------------------------
|
216
|
+
|
217
|
+
def get_config( rqst = nil )
|
218
|
+
scope = "show configuration"
|
219
|
+
scope.concat( " " + rqst ) if rqst
|
220
|
+
begin
|
221
|
+
@ndev.rpc.command( scope, :format => 'text' ).xpath('configuration-output').text
|
222
|
+
rescue NoMethodError
|
223
|
+
# indicates no configuration found
|
224
|
+
nil
|
225
|
+
rescue => e
|
226
|
+
# indicates error in request
|
227
|
+
err = e.rsp.xpath('rpc-error')[0]
|
228
|
+
err_info = err.xpath('error-info/bad-element').text
|
229
|
+
err_msg = err.xpath('error-message').text
|
230
|
+
"ERROR! " + err_msg + ": " + err_info
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
end # class Provider
|
235
|
+
|
236
|
+
|