-
Notifications
You must be signed in to change notification settings - Fork 30
netseg update
Chris Lasell edited this page Nov 18, 2017
·
2 revisions
#!/usr/bin/ruby
### Copyright 2017 Pixar
###
### Licensed under the Apache License, Version 2.0 (the "Apache License")
### with the following modification; you may not use this file except in
### compliance with the Apache License and the following modification to it:
### Section 6. Trademarks. is deleted and replaced with:
###
### 6. Trademarks. This License does not grant permission to use the trade
### names, trademarks, service marks, or product names of the Licensor
### and its affiliates, except as required to comply with Section 4(c) of
### the License and to reproduce the content of the NOTICE file.
###
### You may obtain a copy of the Apache License at
###
### http://www.apache.org/licenses/LICENSE-2.0
###
### Unless required by applicable law or agreed to in writing, software
### distributed under the Apache License with the above modification is
### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
### KIND, either express or implied. See the Apache License for the specific
### language governing permissions and limitations under the Apache License.
# == Synopsis
# Add, remove, or change the Network Segments in the JSS based on data from an input file
# in CSV, tab, or other delimited format.
#
# == Usage
# subnet-update [-t | -d delimiter] [-h] file
#
#
# == Author
# Chris Lasell <chrisl@pixar.com>
#
# == Copyright
# Copyright (c) 2014 Pixar Animation Studios
##############################
# Libraries
##############################
require 'ruby-jss'
require 'getoptlong'
require 'English'
# The app object
##############################
class App
# Constants
##############################
PROG_NAME = File.basename($PROGRAM_NAME)
USAGE = "Usage: #{PROG_NAME} [options] [--help] /path/to/file".freeze
POTENTIAL_COLUMNS = %i(name starting ending cidr mask).freeze
DEFAULT_CACHE_FILE = Pathname.new('~/.last_subnet_update').expand_path
DEFAULT_DELIMITER = "\t".freeze
DEFAULT_COLUMNS = [:name, :starting, :ending].freeze
DEFAULT_MANUAL_PREFIX = 'Manual-'.freeze
# define the cli opts
CLI_OPTS = GetoptLong.new(
['--help', '-H', GetoptLong::NO_ARGUMENT],
['--delimiter', '--delim', '-d', GetoptLong::REQUIRED_ARGUMENT],
['--header', '-h', GetoptLong::NO_ARGUMENT],
['--columns', '-c', GetoptLong::OPTIONAL_ARGUMENT],
['--manual-prefix', '-m', GetoptLong::OPTIONAL_ARGUMENT],
['--cache', GetoptLong::REQUIRED_ARGUMENT],
['--debug', GetoptLong::NO_ARGUMENT],
['--server', '-S', GetoptLong::OPTIONAL_ARGUMENT],
['--port', '-P', GetoptLong::OPTIONAL_ARGUMENT],
['--user', '-U', GetoptLong::OPTIONAL_ARGUMENT],
['--no-verify-cert', '-V', GetoptLong::NO_ARGUMENT],
['--timeout', '-T', GetoptLong::OPTIONAL_ARGUMENT],
['--no-op', '-N', GetoptLong::NO_ARGUMENT]
)
attr_reader :debug
def initialize(_args)
@getpass = $stdin.tty? ? :prompt : :stdin
set_defaults
parse_cli
check_opts
end # init
def set_defaults
@debug = false
@delim = DEFAULT_DELIMITER
@header = false
@columns = DEFAULT_COLUMNS
@cache_file = DEFAULT_CACHE_FILE
@manual_prefix = DEFAULT_MANUAL_PREFIX
@user = JSS::CONFIG.api_username
@server = JSS::CONFIG.api_server_name
end
def parse_cli
# parse the cli opts
CLI_OPTS.each do |opt, arg|
case opt
when '--help' then show_help
when '--delimiter' then @delim = arg
when '--header' then @header = true
when '--columns' then @columns = arg.split(',').map(&:to_sym)
when '--manual-prefix' then @manual_prefix = arg
when '--cache' then @cache_file = Pathname.new arg
when '--debug' then @debug = true
when '--server' then @server = arg
when '--port' then @port = arg
when '--user'then @user = arg
when '--no-verify-cert' then @verify_cert = false
when '--timeout' then @timeout = arg
when '--no-op' then @noop = true
end # case
end # each opt arg
@columns = nil if @columns && @columns.empty?
@file = Pathname.new ARGV.shift.to_s
end # parse_cli
def check_opts
raise JSS::MissingDataError, 'No JSS Username provided or found in the JSS gem config.' unless @user
raise JSS::MissingDataError, 'No JSS Server provided or found in the JSS gem config.' unless @server
raise ArgumentError, "No input file specified.\n#{USAGE}" unless @file
raise "Input file doesn't exist or is not readable: #{@file}" unless @file.readable?
end
# Go!
def run
unless data_file_changed?
puts "File hasn't changed since last time, no changes to make!"
return
end
connect_to_jss
@parsed_data = parse_file
update_network_segments
cache_latest_data
end # run
def connect_to_jss
JSS.api.connect(
server: @server,
port: @port,
verify_cert: @verify_cert,
user: @user,
pw: @getpass,
stdin_line: 1,
timeout: @timeout
)
end
def show_help
puts <<-FULLHELP
Update the JSS Network Segments from a delimited file of subnet information.
CAUTION: This script can delete Network Segments from your JSS.
See the --no-op option
#{USAGE}
Options:
-d, --delimiter - The field delimiter in the file, defaults to tab.
-c, --columns [col1,col2,col3]
- The column order in file, must include 'name', 'starting',
and either 'ending', 'mask', or 'cidr'
-h, --header - The first line of the file is a header line,
defining the columns
-m, --manual-prefix - Network Segment names in the file and the JSS with this
prefix are ignored. Defaults to 'Manual-'
--cache /path/.. - Where read/save the input data for comparison between runs.
Defaults to ~/.last_subnet_update
-S, --server srvr - specify the JSS API server name
-P, --port portnum - specify the JSS API port
-U, --user username - specify the JSS API user
-V, --no-verify-cert - Allow self-signed, unverified SSL certificate
-T, --timeout secs - specify the JSS API timeout
-N, --no-op - Don't make any changes in the JSS, just report what would
have be changed.
-H, --help - show this help
--debug - show the ruby backtrace when errors occur
This program parses the input file line by line (possibly accounting for a header line).
Each line defines the name and IP-range of a network segment.
- If a segment in the file doesn't exist in the JSS, it is created in the JSS.
- If a segment's range is different in the file, it is updated in the JSS.
- If a segment in the JSS doesn't exist in the file, it is deleted from the JSS.
Any network segments with names starting with the given --manual-prefix are ignored.
The default manual-prefix is 'Manual-' so, e.g. segments named 'Manual-isolated'
and 'Manual-special-servers' in the JSS won't be touched.
Input File:
- The file must contain three columns, separated by the --delimiter,
with these names, in any order:
- 'name' (the network segment name)
- 'starting' (the starting IP address of the network segment)
- ONE of:
- 'ending' (the ending IP address of the network segment)
- 'cidr' (the network range of the segment as a CIDR bitmask, e.g. '24')
- 'mask' (the network range of the segment as an IP mask, e.g. '255.255.255.0')
Notes:
- The --columns option is a comma-separted list of the three
column names above indicating the column-order in the file.
- If --columns are not provided, and --header is specified, the first line
is assumed to contain the column names, separated by the delimiter
- If --header is provided with --columns, the first line of the file is ignored.
- The raw data from the file is cached and compared to the input file at
the next run. If the data is identical, no changes are made.
- If no API connection settings are provided, they will be read from
/etc/ruby-jss.conf and ~/.ruby-jss.conf. See the ruby-jss docs for details.
- The password for the connection will be read from STDIN or prompted if needed
FULLHELP
exit 0
end
# parse the incoming data file into an Hash of Hashes,
# Top level keys are the NetSeg names,
# Subhashes have keys :starting, and :ending
# Exclude any with names starting with @manual_prefix
#
# @return [Hash<Hash>] The lines of the file, as hashes
#
def parse_file
puts 'Parsing the data file'
# split the data into an array by newline/return chars.
# this means files saved by excel or windows will work.
lines = @raw_data.split(/[\n\r]+/)
# remove the first line if its a header, and parse it into the columns
# if needed
if @header
header = lines.shift
@columns ||= header.split(/\s*#{@delim}\s*/).map(&:to_sym)
end
check_columns
parsed_data = {}
lines.each do |line|
parsed_line = parse_a_data_line line
next unless parsed_line
name = parsed_line.delete :name
parsed_data[name] = parsed_line
end
parsed_names = parsed_data.keys
jss_names = JSS::NetworkSegment.all_names.reject { |jss_name| jss_name.start_with? @manual_prefix }
@segments_to_add = parsed_names - jss_names
@segments_to_delete = jss_names - parsed_names
@segments_to_check_for_changes = parsed_names - @segments_to_add - @segments_to_delete
parsed_data
end # parse_file
def check_columns
raise "Columns must include 'name' and 'starting'" unless \
@columns.include?(:name) && \
@columns.include?(:starting)
raise "Columns must include either 'ending', 'cidr', or 'mask'" unless \
@columns.include?(:ending) || \
@columns.include?(:cidr) || \
@columns.include?(:mask)
@use_cidr = (@columns.include?(:cidr) || @columns.include?(:mask))
end
def parse_a_data_line(line)
parts = line.split(@delim).map(&:strip)
name = parts[@columns.index(:name)]
starting = parts[@columns.index(:starting)]
ending = parts[@columns.index(:ending)]
unless name && starting && ending
puts "Skipping invalid line: #{line}"
return nil
end
if name.start_with? @manual_prefix
puts "Ignoring line with manual_prefix: #{line}"
return nil
end
{ name: name, starting: starting, ending: ending }
end
def data_file_changed?
# read in the file
@raw_data = @file.read
return true unless @cache_file.exist?
@raw_data != @cache_file.read
end
def cache_latest_data
return if @noop
@cache_file.jss_save @raw_data
end
def update_network_segments
puts 'Applying changes'
add_segments
delete_segments
update_segments
puts 'Done!'
end # update_network_segments
def add_segments
@segments_to_add.each do |seg|
seg_data = @parsed_data[seg]
if @noop
connector = @use_cidr ? '/' : '->'
puts "Without --no-op this would: Add segment named '#{seg}', #{seg_data[:starting]}#{connector}#{seg_data[:ending]}"
next
end # if noop
ender = @use_cidr ? :cidr : :ending_address
new_seg = JSS::NetworkSegment.new(
:id => :new,
:name => seg,
:starting_address => seg_data[:starting],
ender => seg_data[:ending]
)
new_seg.create
puts "Added Network Segment '#{new_seg.name}' to the JSS"
end # @segments_to_add.each do |seg|
end # add_segments
def delete_segments
@segments_to_delete.each do |seg|
if @noop
puts "Without --no-op this would: Delete segment named '#{seg}',"
next
end # if noop
JSS::NetworkSegment.new(name: seg).delete
puts "Deleted Network Segment '#{seg}' from the JSS"
end # @segments_to_delete.each do |seg|
end # delete_segments
def update_segments
@segments_to_check_for_changes.each do |seg|
seg_data = @parsed_data[seg]
data_start = IPAddr.new(seg_data[:starting])
data_end = if @use_cidr
IPAddr.new("#{seg_data[:starting]}/#{seg_data[:ending]}").to_range.end.mask 32
else
IPAddr.new(seg_data[:ending])
end
this_seg = JSS::NetworkSegment.new name: seg
data_range = data_start..data_end
next if this_seg.range == data_range
if @noop
connector = @use_cidr ? '/' : '->'
puts "Without --no-op this would: Update segment named '#{seg}', #{seg_data[:starting]}#{connector}#{seg_data[:ending]}"
next
end # if noop
this_seg.set_ip_range starting_address: data_start, ending_address: data_end
this_seg.save
puts "Updated Network Segment '#{seg}' to range #{data_start} - #{data_end}"
end # @segments_to_check_for_changes.each do |seg|
end # update segments
end # app
##############################
# create the app and go
begin
debug = ARGV.include? '--debug'
app = App.new(ARGV)
app.run
rescue
# handle exceptions not handled elsewhere
puts "An error occurred: #{$ERROR_INFO}"
puts 'Backtrace:' if debug
puts $ERROR_POSITION if debug
end