jss-api 0.5.5 → 0.5.6

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,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