jss-api 0.5.5 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/ruby
2
+
3
+ ### Copyright 2014 Pixar
4
+ ###
5
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
6
+ ### with the following modification; you may not use this file except in
7
+ ### compliance with the Apache License and the following modification to it:
8
+ ### Section 6. Trademarks. is deleted and replaced with:
9
+ ###
10
+ ### 6. Trademarks. This License does not grant permission to use the trade
11
+ ### names, trademarks, service marks, or product names of the Licensor
12
+ ### and its affiliates, except as required to comply with Section 4(c) of
13
+ ### the License and to reproduce the content of the NOTICE file.
14
+ ###
15
+ ### You may obtain a copy of the Apache License at
16
+ ###
17
+ ### http://www.apache.org/licenses/LICENSE-2.0
18
+ ###
19
+ ### Unless required by applicable law or agreed to in writing, software
20
+ ### distributed under the Apache License with the above modification is
21
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22
+ ### KIND, either express or implied. See the Apache License for the specific
23
+ ### language governing permissions and limitations under the Apache License.
24
+
25
+ ##############################
26
+ # == Synopsis
27
+ # Add, remove, or change the Network Segments in the JSS based on data from an input file
28
+ # in CSV, tab, or other delimited format.
29
+ #
30
+ # == Usage
31
+ # subnet-update [-t | -d delimiter] [-h] file
32
+ #
33
+ #
34
+ # == Author
35
+ # Chris Lasell <chrisl@pixar.com>
36
+ #
37
+ # == Copyright
38
+ # Copyright (c) 2014 Pixar Animation Studios
39
+
40
+ ##############################
41
+ # Libraries
42
+ require 'jss-api'
43
+ require 'getoptlong'
44
+
45
+ ##############################
46
+ # The app object
47
+ class App
48
+
49
+ ##############################
50
+ # Constants
51
+
52
+ USAGE = "Usage: #{File.basename($0)} [-d delim] [--header] [-c col1,col2,col3 ] [-m manual-prefix] [--help] /path/to/file"
53
+
54
+ POTENTIAL_COLUMNS = [:name, :starting, :ending, :cidr]
55
+
56
+ # Whenever we process a file, we store it here. The next time we
57
+ # run, if the input file is identical to this, we exit witout doing anything.
58
+ DEFAULT_CACHE_FILE = Pathname.new("~/.last_subnet_update").expand_path
59
+
60
+ DEFAULT_DELIMITER = "\t"
61
+ DEFAULT_COLUMNS = [:name, :starting, :ending]
62
+ DEFAULT_MANUAL_PREFIX = "Manual-"
63
+
64
+
65
+ attr_reader :debug
66
+
67
+ ###############
68
+ # set up
69
+ def initialize(args)
70
+
71
+ # set defaults
72
+ @debug = false
73
+
74
+ @delim = DEFAULT_DELIMITER
75
+ @header = false
76
+ @columns = DEFAULT_COLUMNS
77
+ @cache_file = DEFAULT_CACHE_FILE
78
+ @manual_prefix = DEFAULT_MANUAL_PREFIX
79
+
80
+
81
+ #define the cli opts
82
+ cli_opts = GetoptLong.new(
83
+ [ '--help', '-H', GetoptLong::NO_ARGUMENT ],
84
+ [ '--delimiter', '--delim', '-d', GetoptLong::REQUIRED_ARGUMENT],
85
+ [ '--header', '-h', GetoptLong::NO_ARGUMENT],
86
+ [ '--columns', '-c', GetoptLong::OPTIONAL_ARGUMENT],
87
+ [ '--manual-prefix', '-m', GetoptLong::OPTIONAL_ARGUMENT],
88
+ [ '--cache', GetoptLong::REQUIRED_ARGUMENT ],
89
+ [ '--debug', GetoptLong::NO_ARGUMENT],
90
+ [ '--server', '-S', GetoptLong::OPTIONAL_ARGUMENT],
91
+ [ '--port', '-P', GetoptLong::OPTIONAL_ARGUMENT],
92
+ [ '--user', '-U', GetoptLong::OPTIONAL_ARGUMENT],
93
+ [ '--no-verify-cert', '-V', GetoptLong::NO_ARGUMENT],
94
+ [ '--timeout', '-T', GetoptLong::OPTIONAL_ARGUMENT]
95
+ )
96
+
97
+ # parse the cli opts
98
+ cli_opts.each do |opt, arg|
99
+ case opt
100
+ when '--help' then show_help
101
+ when '--delimiter' then @delim = arg
102
+ when '--header' then @header = true
103
+ when '--columns' then @columns = arg.split(',').map{|c| c.to_sym}
104
+ when '--manual-prefix' then @manual_prefix = arg
105
+ when '--cache' then @cache_file = Pathname.new arg
106
+ when '--debug' then @debug = true
107
+ when '--server'
108
+ @server = arg
109
+
110
+ when '--port'
111
+ @port = arg
112
+
113
+ when '--user'
114
+ @user = arg
115
+
116
+ when '--no-verify-cert'
117
+ @verify_cert = false
118
+
119
+ when '--timeout'
120
+ @timeout = arg
121
+
122
+ end # case
123
+ end # each opt arg
124
+
125
+
126
+ @columns = nil if @columns and @columns.empty?
127
+
128
+ @file = args.shift
129
+
130
+
131
+ end # init
132
+
133
+ ###############
134
+ # Go!
135
+ def run
136
+
137
+ unless @file
138
+ puts "No input file specified."
139
+ puts USAGE
140
+ return
141
+ end
142
+
143
+ @file = Pathname.new @file
144
+
145
+ unless parse_file
146
+ puts "File hasn't changed since last time, no changes to make!"
147
+ return
148
+ end
149
+
150
+ # use any config settings defined....
151
+ @user ||= JSS::CONFIG.api_username
152
+ @server ||= JSS::CONFIG.api_server_name
153
+ @getpass = $stdin.tty? ? :prompt : :stdin
154
+
155
+ raise JSS::MissingDataError, "No JSS Username provided or found in the JSS gem config." unless @user
156
+ raise JSS::MissingDataError, "No JSS Server provided or found in the JSS gem config." unless @server
157
+
158
+ JSS::API.connect( :server => @server,
159
+ :port => @port,
160
+ :verify_cert => @verify_cert,
161
+ :user => @user,
162
+ :pw => @getpass,
163
+ :stdin_line => 1,
164
+ :timeout => @timeout
165
+ )
166
+
167
+ update_network_segments
168
+
169
+ end # run
170
+
171
+ #####################################
172
+ ###
173
+ ### Show Help
174
+ ###
175
+ def show_help
176
+ puts <<-FULLHELP
177
+ Update the JSS Network Segments from a delimited file of subnet information.
178
+
179
+ #{USAGE}
180
+
181
+ Options:
182
+ -d, --delimiter - The field delimiter in the file, defaults to tab.
183
+ -c, --columns [col1,col2,col3]
184
+ - The column order in file, must include 'name', 'starting',
185
+ and either 'ending' or 'cidr'
186
+ -h, --header - The first line of the file is a header line,
187
+ possibly defining the columns
188
+ -m, --manual-prefix - Network Segment names in the JSS with this prefix are ignored.
189
+ Defaults to 'Manual-'
190
+ --cache /path/.. - Where read/save the input data for comparison between runs.
191
+ Defaults to ~/.last_subnet_update
192
+ -S, --server srvr - specify the JSS API server name
193
+ -P, --port portnum - specify the JSS API port
194
+ -U, --user username - specify the JSS API user
195
+ -V, --no-verify-cert - Allow self-signed, unverified SSL certificate
196
+ -T, --timeout secs - specify the JSS API timeout
197
+ -H, --help - show this help
198
+ --debug - show the ruby backtrace when errors occur
199
+
200
+ This program parses the input file line by line (possibly accounting for a header line).
201
+ Each line defines the name and IP-range of a subnet/network segment.
202
+
203
+ - If a segment doesn't exist in the JSS, it is created.
204
+ - If a segment's range has changed, it is updated in the JSS.
205
+ - If a JSS segment doesn't exist in the file, it is deleted from the JSS
206
+ unless its name starts with the --manual-prefix
207
+
208
+ Input File:
209
+ - The file must contain three columns, separated by the --delimiter,
210
+ with these names, in any order:
211
+ - 'name' (the network segment name)
212
+ - 'starting' (the starting IP address of the network segment)
213
+ - EITHER of:
214
+ - 'ending' (the ending IP address of the network segment)
215
+ - 'cidr' (the network range of the segment as a CIDR bitmask, e.g. '24')
216
+ Notes:
217
+ - The --columns option is a comma-separted list of the three
218
+ column names aboveindicating the column-order in the file.
219
+
220
+ - If --columns are not provided, and --header is specified, the first line
221
+ is assumed to contain the column names, separated by the delimiter
222
+
223
+ - If --header is provided with --columns, the first line of the file is ignored.
224
+
225
+ - The raw data from the file is cached and compared to the input file at
226
+ the next run. If the data is identical, no JSS connection is made.
227
+
228
+ - If no API settings are provided, they will be read from /etc/jss_gem.conf
229
+ and ~/.jss_gem.conf. See the JSS Gem docs for details.
230
+
231
+ - The password for the connection will be read from STDIN or prompted if needed
232
+
233
+ FULLHELP
234
+ exit 0
235
+ end
236
+
237
+ ########################
238
+ # parse the incoming data file.
239
+ # If the file hasn't changed from the last time we processed it
240
+ # then return false
241
+ # otherwise parse it into @parsed_data and return true
242
+ # @parsed_data is an array of hashes, each with :name, :starting, and :ending
243
+ #
244
+ def parse_file
245
+
246
+ raise "'#{@file}' is not readable, or not a regular file" unless @file.readable? and @file.file?
247
+
248
+ # read in the file
249
+ @raw_data = @file.read
250
+
251
+ # compare it to the one we used last time
252
+ if @cache_file.readable?
253
+ return false if @raw_data == @cache_file.read
254
+ end
255
+
256
+ # split the data into an array by newlines
257
+ lines = @raw_data.split "\n"
258
+
259
+ # remove the first line if its a header, and parse it into the columns
260
+ # if needed
261
+ if @header
262
+ header = lines.shift
263
+ @columns ||= header.split(/\s*#{@delim}\s*/).map{|c| c.to_sym}
264
+ end
265
+
266
+ # check some state
267
+ raise "Columns must include 'name' and 'starting'" unless @columns.include?(:name) and @columns.include?(:starting)
268
+ raise "Columns must include either 'ending' or 'cidr'" unless @columns.include?(:ending) or @columns.include?(:cidr)
269
+
270
+ @use_cidr = @columns.include? :cidr
271
+
272
+
273
+ # which columns are which in the file?
274
+ name = @columns.index :name
275
+ starting = @columns.index :starting
276
+ ending = @use_cidr ? @columns.index(:cidr) : @columns.index(:ending)
277
+
278
+ # split each line and convert it into a hash
279
+ @parsed_data = lines.map do |line|
280
+
281
+ parts = line.split(@delim).map{|f| f.strip }
282
+
283
+ unless parts[name] and parts[starting] and parts[ending]
284
+ puts "Skipping invalid line: #{line}"
285
+ next
286
+ end
287
+
288
+
289
+ {:name => parts[name], :starting => parts[starting], :ending => parts[ending]}
290
+ end
291
+
292
+ # parsed data is now an array of hashes
293
+ return true
294
+ end
295
+
296
+ #############################################
297
+ #############################################
298
+ # Update the JSS Network Segments from GIT_NETBLOCKS_URL, q.v.
299
+ def update_network_segments
300
+
301
+ # CREATE any that are in the parsed data but not yet in the JSS,
302
+ # and UPDATE any that exist but have modified ranges.
303
+ # While looping through, make a hash of JSS::NetworkSegment objects, keyed by their name.
304
+ segs_from_data = {}
305
+
306
+ @parsed_data.each do |pd|
307
+
308
+ # skip anthing with the manual prefix
309
+ next if pd[:name].start_with? @manual_prefix
310
+
311
+ ender = @use_cidr ? :cidr : :ending_address
312
+
313
+ begin
314
+ this_seg = JSS::NetworkSegment.new(:id => :new, :name => pd[:name], :starting_address => pd[:starting], ender => pd[:ending])
315
+
316
+ # If the new netsegment should have other settings (dist. point, netboot server, etc...)
317
+ # here's where you should apply those settings.
318
+
319
+ this_seg.create
320
+ puts "Added Network Segment '#{this_seg.name}' to the JSS"
321
+
322
+ # it already exists, so see if it needs any changes
323
+ rescue JSS::AlreadyExistsError
324
+
325
+ # there's already one with this name, so just grab it.
326
+ this_seg = JSS::NetworkSegment.new( :name => pd[:name])
327
+
328
+ # does the startng addres need to be changed?
329
+ needs_update = this_seg.starting_address.to_s != pd[:starting].to_s
330
+
331
+ # even if we don't need to update the starting, we might need to update
332
+ # the ending...
333
+ unless needs_update
334
+ if @use_cidr
335
+ needs_update = this_seg.cidr.to_i != pd[:ending].to_i
336
+ else
337
+ needs_update = this_seg.ending_address.to_s != pd[:ending].to_s
338
+ end # if @use_cidr
339
+ end #unless needs update
340
+
341
+ # did we decide we need an update?
342
+ if needs_update
343
+ this_seg.starting_address = pd[:starting]
344
+ if @use_cidr
345
+ this_seg.cidr = pd[:ending].to_i
346
+ else
347
+ this_seg.ending_address = pd[:ending]
348
+ end # if @use_cidr
349
+ this_seg.update
350
+ puts "Updated IP range for Network Segment '#{this_seg.name}'"
351
+
352
+ else # doesn't need update
353
+ puts "Network Segment '#{this_seg.name}' doesn't have any changes."
354
+ end # if needs update
355
+
356
+ # rescue other errors
357
+ rescue
358
+ raise "There was an error with NetworkSegment #{pd[:name]}: #{$!}"
359
+ end # begin
360
+
361
+ segs_from_data[this_seg.name] = this_seg
362
+ end
363
+
364
+
365
+ # DELETE those in jss, but not in parsed data,
366
+ # unless the name starts with @manual_prefix
367
+ JSS::NetworkSegment.map_all_ids_to(:name).each do |id,name|
368
+
369
+ next if name.start_with? @manual_prefix
370
+
371
+ unless segs_from_data.keys.include? name
372
+ JSS::NetworkSegment.new(:id => id).delete
373
+ puts "Deleted Network Segment '#{name}' from the JSS"
374
+ end # unless
375
+
376
+ end # jss_uids.each seg
377
+
378
+ # save the data into a file for comparison next time
379
+ @cache_file.jss_save @raw_data
380
+
381
+ # all done
382
+ return true
383
+ end # update_network_segments
384
+
385
+ end # app
386
+
387
+ ##############################
388
+ # create the app and go
389
+ begin
390
+ app = App.new(ARGV)
391
+ app.run
392
+ rescue
393
+ # handle exceptions not handled elsewhere
394
+ puts "An error occurred: #{$!}"
395
+ puts "Backtrace:" if app.debug
396
+ puts $@ if app.debug
397
+
398
+ ensure
399
+
400
+ end
@@ -77,7 +77,10 @@ module JSS
77
77
  ### Default timeouts in seconds
78
78
  DFT_OPEN_TIMEOUT = 60
79
79
  DFT_TIMEOUT = 60
80
-
80
+
81
+ ### The Default SSL Version
82
+ DFT_SSL_VERSION = 'TLSv1'
83
+
81
84
  #####################################
82
85
  ### Attributes
83
86
  #####################################
@@ -115,15 +118,15 @@ module JSS
115
118
  ###
116
119
  ### @param args[Hash] the keyed arguments for connection.
117
120
  ###
118
- ### @option args :server[String] Required, the hostname of the JSS API server
121
+ ### @option args :server[String] the hostname of the JSS API server, required if not defined in JSS::CONFIG
119
122
  ###
120
123
  ### @option args :port[Integer] the port number to connect with, defaults to 8443
121
124
  ###
122
- ### @option args :verify_cert[Boolean]should HTTPS SSL certificates be verified. Defaults to true.
125
+ ### @option args :verify_cert[Boolean] should HTTPS SSL certificates be verified. Defaults to true.
123
126
  ### If your connection raises RestClient::SSLCertificateNotVerified, and you don't care about the
124
127
  ### validity of the SSL cert. just set this explicitly to false.
125
128
  ###
126
- ### @option args :user[String] Required, a JSS user who as API privs
129
+ ### @option args :user[String] a JSS user who has API privs, required if not defined in JSS::CONFIG
127
130
  ###
128
131
  ### @option args :pw[String,Symbol] Required, the password for that user, or :prompt, or :stdin
129
132
  ### If :prompt, the user is promted on the commandline to enter the password for the :user.
@@ -145,6 +148,7 @@ module JSS
145
148
  args[:user] ||= JSS::CONFIG.api_username
146
149
  args[:timeout] ||= JSS::CONFIG.api_timeout
147
150
  args[:open_timeout] ||= JSS::CONFIG.api_timeout_open
151
+ args[:ssl_version] ||= JSS::CONFIG.api_ssl_version
148
152
 
149
153
  # if verify cert given was NOT in the args....
150
154
  if args[:verify_cert].nil?
@@ -156,12 +160,20 @@ module JSS
156
160
  args[:port] ||= SSL_PORT
157
161
  args[:timeout] ||= DFT_TIMEOUT
158
162
  args[:open_timeout] ||= DFT_OPEN_TIMEOUT
159
-
163
+
164
+ # As of Casper 9.61 we can't use SSL, must use TLS, since SSLv3 was susceptible to poodles.
165
+ # NOTE - this requires rest-client v 1.7.0 or higher
166
+ # which requires mime-types 2.0 or higher, which requires ruby 1.9.2 or higher!
167
+ # That means that support for ruby 1.8.7 stops with Casper 9.6
168
+ args[:ssl_version] ||= DFT_SSL_VERSION
169
+
170
+
160
171
  # must have server, user, and pw
161
- raise JSS::MissingDataError, "Missing :server" unless args[:server]
162
- raise JSS::MissingDataError, "Missing :user" unless args[:user]
172
+ raise JSS::MissingDataError, "No JSS :server specified, or in configuration." unless args[:server]
173
+ raise JSS::MissingDataError, "No JSS :user specified, or in configuration." unless args[:user]
163
174
  raise JSS::MissingDataError, "Missing :pw for user '#{args[:user]}'" unless args[:pw]
164
-
175
+
176
+ # ssl or not?
165
177
  ssl = SSL_PORT == args[:port].to_i ? "s" : ''
166
178
  @rest_url = URI::encode "http#{ssl}://#{args[:server]}:#{args[:port]}/#{RSRC}"
167
179
 
@@ -169,9 +181,6 @@ module JSS
169
181
  # if verify_cert is nil (unset) or non-false, then we will verify
170
182
  args[:verify_ssl] = (args[:verify_cert].nil? or args[:verify_cert]) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
171
183
 
172
- # make sure we have a user
173
- raise JSS::MissingDataError, "No JSS user specified, or listed in configuration." unless args[:user]
174
-
175
184
  args[:password] = if args[:pw] == :prompt
176
185
  JSS.prompt_for_password "Enter the password for JSS user '#{args[:user]}':"
177
186
  elsif args[:pw].is_a?(Symbol) and args[:pw].to_s.start_with?('stdin')
@@ -183,6 +192,8 @@ module JSS
183
192
  args[:pw]
184
193
  end
185
194
 
195
+
196
+
186
197
  # heres our connection
187
198
  @cnx = RestClient::Resource.new("#{@rest_url}", args)
188
199