From 73dbf2a5fa30a2ea342dbb530527b5437fa5403a Mon Sep 17 00:00:00 2001 From: cn Date: Fri, 28 Feb 2020 15:13:28 +0100 Subject: [PATCH] gem: add solargraph support --- .gitignore | 1 + .solargraph.yml | 16 ++++++++ .travis.yml | 3 ++ CHANGELOG.md | 4 +- Rakefile | 12 ++++++ dyndnsd.gemspec | 1 + lib/dyndnsd.rb | 39 ++++++++++++++++++- lib/dyndnsd/database.rb | 4 ++ lib/dyndnsd/generator/bind.rb | 4 ++ lib/dyndnsd/helper.rb | 16 ++++++++ lib/dyndnsd/responder/dyndns_style.rb | 15 +++++++ lib/dyndnsd/responder/rest_style.rb | 15 +++++++ lib/dyndnsd/textfile_reporter.rb | 13 +++++++ lib/dyndnsd/updater/command_with_bind_zone.rb | 4 ++ 14 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 .solargraph.yml diff --git a/.gitignore b/.gitignore index 8c23188..36f26a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store *.lock pkg/* +.yardoc diff --git a/.solargraph.yml b/.solargraph.yml new file mode 100644 index 0000000..a01ce98 --- /dev/null +++ b/.solargraph.yml @@ -0,0 +1,16 @@ +--- +include: +- "**/*.rb" +- "bin/dyndnsd" +exclude: +- spec/**/* +- test/**/* +- vendor/**/* +- ".bundle/**/*" +require: [] +domains: [] +reporters: +- rubocop +- require_not_found +require_paths: [] +max_files: 5000 diff --git a/.travis.yml b/.travis.yml index a305b63..2920897 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,6 @@ rvm: - 2.5 - 2.4 - 2.3 + +script: +- bundle exec rake travis diff --git a/CHANGELOG.md b/CHANGELOG.md index c671c2e..de50cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ IMPROVEMENTS: - Add Ruby 2.7 support +- Add [solargraph](https://github.com/castwide/solargraph) to dev tooling as Ruby Language Server usable e.g. for IDEs (used solargraph version not compatible with Ruby 2.7 as bundler-audit 0.6.x requires old `thor` gem) +- Document code using YARD tags, e.g. for type information and better code completion ## 2.0.0 (January 25, 2019) @@ -15,7 +17,7 @@ IMPROVEMENTS: - Better code maintainability by refactorings - Update dependencies, mainly `rack` to new major version 2 - Add Ruby 2.5 and Ruby 2.6 support -- Add experimental [OpenTracing](http://opentracing.io/) support with [CNCF Jaeger](https://github.com/jaegertracing/jaeger) +- Add experimental [OpenTracing](https://opentracing.io/) support with [CNCF Jaeger](https://github.com/jaegertracing/jaeger) - Support host offlining by deleting the associated DNS records - Add textfile reporter to write Graphite-style metrics (also compatible with [Prometheus](https://prometheus.io/)) into a file diff --git a/Rakefile b/Rakefile index 538a5a5..ba57a1f 100644 --- a/Rakefile +++ b/Rakefile @@ -7,4 +7,16 @@ RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new Bundler::Audit::Task.new +desc 'Should be run by developer once to prepare initial solargraph usage (fill caches etc.)' +task :'solargraph:init' do + sh 'solargraph download-core' +end + +desc 'Run experimental solargraph type checker' +task :'solargraph:tc' do + sh 'solargraph typecheck' +end + task default: [:rubocop, :spec, 'bundle:audit'] + +task travis: [:default, :'solargraph:tc'] diff --git a/dyndnsd.gemspec b/dyndnsd.gemspec index 928cfa2..e898539 100644 --- a/dyndnsd.gemspec +++ b/dyndnsd.gemspec @@ -33,4 +33,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'rack-test' s.add_development_dependency 'rubocop', '~> 0.80.0' s.add_development_dependency 'bundler-audit', '~> 0.6.0' + s.add_development_dependency 'solargraph' end diff --git a/lib/dyndnsd.rb b/lib/dyndnsd.rb index 2981f83..20ecea6 100755 --- a/lib/dyndnsd.rb +++ b/lib/dyndnsd.rb @@ -21,21 +21,32 @@ require 'dyndnsd/textfile_reporter' require 'dyndnsd/version' module Dyndnsd + # @return [Logger] def self.logger @logger end + # @param logger [Logger] + # @return [Logger] def self.logger=(logger) @logger = logger end class LogFormatter + # @param lvl [Object] + # @param _time [DateTime] + # @param _progname [String] + # @param msg [Object] + # @return [String] def call(lvl, _time, _progname, msg) format("[%s] %-5s %s\n", Time.now.strftime('%Y-%m-%d %H:%M:%S'), lvl, msg.to_s) end end class Daemon + # @param config [Hash{String => Object}] + # @param db [Dyndnsd::Database] + # @param updater [#update] def initialize(config, db, updater) @users = config['users'] @domain = config['domain'] @@ -51,6 +62,9 @@ module Dyndnsd end end + # @param username [String] + # @param password [String] + # @return [Boolean] def authorized?(username, password) Helper.span('check_authorized') do |span| span.set_tag('dyndnsd.user', username) @@ -64,6 +78,8 @@ module Dyndnsd end end + # @param env [Hash{String => String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def call(env) return [422, {'X-DynDNS-Response' => 'method_forbidden'}, []] if env['REQUEST_METHOD'] != 'GET' return [422, {'X-DynDNS-Response' => 'not_found'}, []] if env['PATH_INFO'] != '/nic/update' @@ -71,6 +87,7 @@ module Dyndnsd handle_dyndns_request(env) end + # @return [void] def self.run! if ARGV.length != 1 puts 'Usage: dyndnsd config_file' @@ -109,6 +126,8 @@ module Dyndnsd private + # @param params [Hash{String => String}] + # @return [Array{String}] def extract_v4_and_v6_address(params) return [] if !(params['myip']) begin @@ -120,6 +139,9 @@ module Dyndnsd end end + # @param env [Hash{String => String}] + # @param params [Hash{String => String}] + # @return [Array{String}] def extract_myips(env, params) # require presence of myip parameter as valid IPAddr (v4) and valid myip6 return extract_v4_and_v6_address(params) if params.key?('myip6') @@ -134,6 +156,9 @@ module Dyndnsd [env['REMOTE_ADDR']] end + # @param hostnames [String] + # @param myips [Array{String}] + # @return [Array{Symbol}] def process_changes(hostnames, myips) changes = [] Helper.span('process_changes') do |span| @@ -158,6 +183,7 @@ module Dyndnsd changes end + # @return [void] def update_db @db['serial'] += 1 Dyndnsd.logger.info "Committing update ##{@db['serial']}" @@ -166,6 +192,8 @@ module Dyndnsd Metriks.meter('updates.committed').mark end + # @param env [Hash{String => String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def handle_dyndns_request(env) params = Rack::Utils.parse_query(env['QUERY_STRING']) @@ -204,6 +232,8 @@ module Dyndnsd # SETUP + # @param config [Hash{String => Object}] + # @return [void] private_class_method def self.setup_logger(config) if config['logfile'] Dyndnsd.logger = Logger.new(config['logfile']) @@ -215,6 +245,7 @@ module Dyndnsd Dyndnsd.logger.formatter = LogFormatter.new end + # @return [void] private_class_method def self.setup_traps Signal.trap('INT') do Dyndnsd.logger.info 'Quitting...' @@ -226,6 +257,8 @@ module Dyndnsd end end + # @param config [Hash{String => Object}] + # @return [void] private_class_method def self.setup_monitoring(config) # configure metriks if config['graphite'] @@ -253,6 +286,8 @@ module Dyndnsd end end + # @param config [Hash{String => Object}] + # @return [void] private_class_method def self.setup_tracing(config) # configure OpenTracing if config.dig('tracing', 'jaeger') @@ -267,10 +302,12 @@ module Dyndnsd end end + # @param config [Hash{String => Object}] + # @return [void] private_class_method def self.setup_rack(config) # configure daemon db = Database.new(config['db']) - updater = Updater::CommandWithBindZone.new(config['domain'], config['updater']['params']) if config['updater']['name'] == 'command_with_bind_zone' + updater = Updater::CommandWithBindZone.new(config['domain'], config.dig('updater', 'params')) if config.dig('updater', 'name') == 'command_with_bind_zone' daemon = Daemon.new(config, db, updater) # configure rack diff --git a/lib/dyndnsd/database.rb b/lib/dyndnsd/database.rb index 7ae6c21..daedef3 100644 --- a/lib/dyndnsd/database.rb +++ b/lib/dyndnsd/database.rb @@ -7,10 +7,12 @@ module Dyndnsd def_delegators :@db, :[], :[]=, :each, :has_key? + # @param db_file [String] def initialize(db_file) @db_file = db_file end + # @return [void] def load if File.file?(@db_file) @db = JSON.parse(File.open(@db_file, 'r', &:read)) @@ -20,6 +22,7 @@ module Dyndnsd @db_hash = @db.hash end + # @return [void] def save Helper.span('database_save') do |_span| File.open(@db_file, 'w') { |f| JSON.dump(@db, f) } @@ -27,6 +30,7 @@ module Dyndnsd end end + # @return [Boolean] def changed? @db_hash != @db.hash end diff --git a/lib/dyndnsd/generator/bind.rb b/lib/dyndnsd/generator/bind.rb index 213ff85..17c704e 100644 --- a/lib/dyndnsd/generator/bind.rb +++ b/lib/dyndnsd/generator/bind.rb @@ -2,6 +2,8 @@ module Dyndnsd module Generator class Bind + # @param domain [String] + # @param config [Hash{String => Object}] def initialize(domain, config) @domain = domain @ttl = config['ttl'] @@ -10,6 +12,8 @@ module Dyndnsd @additional_zone_content = config['additional_zone_content'] end + # @param db [Dyndnsd::Database] + # @return [String] def generate(db) out = [] out << "$TTL #{@ttl}" diff --git a/lib/dyndnsd/helper.rb b/lib/dyndnsd/helper.rb index 0184735..9ac124f 100644 --- a/lib/dyndnsd/helper.rb +++ b/lib/dyndnsd/helper.rb @@ -3,6 +3,9 @@ require 'ipaddr' module Dyndnsd class Helper + # @param hostname [String] + # @param domain [String] + # @return [Boolean] def self.fqdn_valid?(hostname, domain) return false if hostname.length < domain.length + 2 return false if !hostname.end_with?(domain) @@ -11,6 +14,8 @@ module Dyndnsd true end + # @param ip [String] + # @return [Boolean] def self.ip_valid?(ip) IPAddr.new(ip) true @@ -18,15 +23,26 @@ module Dyndnsd false end + # @param username [String] + # @param password [String] + # @param users [Hash] + # @return [Boolean] def self.user_allowed?(username, password, users) (users.key? username) && (users[username]['password'] == password) end + # @param hostname [String] + # @param myips [Array] + # @param hosts [Hash] + # @return [Boolean] def self.changed?(hostname, myips, hosts) # myips order is always deterministic ((!hosts.include? hostname) || (hosts[hostname] != myips)) && !myips.empty? end + # @param operation [String] + # @param block [Proc] + # @return [void] def self.span(operation, &block) scope = OpenTracing.start_active_span(operation) span = scope.span diff --git a/lib/dyndnsd/responder/dyndns_style.rb b/lib/dyndnsd/responder/dyndns_style.rb index 51f829a..a470823 100644 --- a/lib/dyndnsd/responder/dyndns_style.rb +++ b/lib/dyndnsd/responder/dyndns_style.rb @@ -2,10 +2,13 @@ module Dyndnsd module Responder class DynDNSStyle + # @param app [#call] def initialize(app) @app = app end + # @param env [Hash{String => String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def call(env) @app.call(env).tap do |status_code, headers, body| if headers.key?('X-DynDNS-Response') @@ -18,6 +21,10 @@ module Dyndnsd private + # @param status_code [Integer] + # @param headers [Hash{String => String}] + # @param body [Array{String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def decorate_dyndnsd_response(status_code, headers, body) if status_code == 200 [200, {'Content-Type' => 'text/plain'}, [get_success_body(body[0], body[1])]] @@ -26,6 +33,10 @@ module Dyndnsd end end + # @param status_code [Integer] + # @param headers [Hash{String => String}] + # @param _body [Array{String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def decorate_other_response(status_code, headers, _body) if status_code == 400 [status_code, headers, ['Bad Request']] @@ -34,10 +45,14 @@ module Dyndnsd end end + # @param changes [Array{Symbol}] + # @param myips [Array{String}] + # @return [String] def get_success_body(changes, myips) changes.map { |change| "#{change} #{myips.join(' ')}" }.join("\n") end + # @return [Hash{String => Object}] def error_response_map { # general http errors diff --git a/lib/dyndnsd/responder/rest_style.rb b/lib/dyndnsd/responder/rest_style.rb index e2e0b8d..8c89ccb 100644 --- a/lib/dyndnsd/responder/rest_style.rb +++ b/lib/dyndnsd/responder/rest_style.rb @@ -2,10 +2,13 @@ module Dyndnsd module Responder class RestStyle + # @param app [#call] def initialize(app) @app = app end + # @param env [Hash{String => String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def call(env) @app.call(env).tap do |status_code, headers, body| if headers.key?('X-DynDNS-Response') @@ -18,6 +21,10 @@ module Dyndnsd private + # @param status_code [Integer] + # @param headers [Hash{String => String}] + # @param body [Array{String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def decorate_dyndnsd_response(status_code, headers, body) if status_code == 200 [200, {'Content-Type' => 'text/plain'}, [get_success_body(body[0], body[1])]] @@ -26,6 +33,10 @@ module Dyndnsd end end + # @param status_code [Integer] + # @param headers [Hash{String => String}] + # @param _body [Array{String}] + # @return [Array{Integer,Hash{String => String},Array{String}}] def decorate_other_response(status_code, headers, _body) if status_code == 400 [status_code, headers, ['Bad Request']] @@ -34,10 +45,14 @@ module Dyndnsd end end + # @param changes [Array{Symbol}] + # @param myips [Array{String}] + # @return [String] def get_success_body(changes, myips) changes.map { |change| change == :good ? "Changed to #{myips.join(' ')}" : "No change needed for #{myips.join(' ')}" }.join("\n") end + # @return [Hash{String => Object}] def error_response_map { # general http errors diff --git a/lib/dyndnsd/textfile_reporter.rb b/lib/dyndnsd/textfile_reporter.rb index a19993c..73aeae6 100644 --- a/lib/dyndnsd/textfile_reporter.rb +++ b/lib/dyndnsd/textfile_reporter.rb @@ -5,8 +5,11 @@ require 'metriks' module Dyndnsd class TextfileReporter + # @return [String] attr_reader :file + # @param file [String] + # @param options [Hash{Symbol => Object}] def initialize(file, options = {}) @file = file @@ -17,6 +20,7 @@ module Dyndnsd @on_error = options[:on_error] || proc { |ex| } end + # @return [void] def start @thread ||= Thread.new do loop do @@ -33,16 +37,19 @@ module Dyndnsd end end + # @return [void] def stop @thread&.kill @thread = nil end + # @return [void] def restart stop start end + # @return [void] def write File.open(@file, 'w') do |f| @registry.each do |name, metric| @@ -85,6 +92,12 @@ module Dyndnsd end end + # @param file [String] + # @param base_name [String] + # @param metric [Object] + # @param keys [Array{Symbol}] + # @param snapshot_keys [Array{Symbol}] + # @return [void] def write_metric(file, base_name, metric, keys, snapshot_keys = []) time = Time.now.to_i diff --git a/lib/dyndnsd/updater/command_with_bind_zone.rb b/lib/dyndnsd/updater/command_with_bind_zone.rb index d15dfd7..8ac236b 100644 --- a/lib/dyndnsd/updater/command_with_bind_zone.rb +++ b/lib/dyndnsd/updater/command_with_bind_zone.rb @@ -2,12 +2,16 @@ module Dyndnsd module Updater class CommandWithBindZone + # @param domain [String] + # @param config [Hash{String => Object}] def initialize(domain, config) @zone_file = config['zone_file'] @command = config['command'] @generator = Generator::Bind.new(domain, config) end + # @param db [Dyndnsd::Database] + # @return [void] def update(db) Helper.span('updater_update') do |span| span.set_tag('dyndnsd.updater.name', self.class.name.split('::').last)