-
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
# == 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
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.