From 052cbfa263e4ad09b2a0cd0c99c3a78649ee2181 Mon Sep 17 00:00:00 2001 From: Christopher Vollick <0@psycoti.ca> Date: Fri, 17 Mar 2023 16:34:12 -0400 Subject: [PATCH] Porting Script This does a few things. It's meant to be run headlessly, but it can also be run by a human if they want to do it manually. So in pursuit of that it has a few options. The defaults ignore informational logs, send exceptions to sentry, warnings to a pubsub channel, and send messages to the customer to tell them the port is finished. But the manual mode logs informational messages, warnings, and errors all to the terminal. And then it can either also log the things it would have said to the customer, or it can send those automatically still. I've also got an option for making fake ports. Yeah, it's just used when testing the thing, but given that I run it that way 90% of the time, it felt weird to just have it floating around in my working dir and having to keep remembering to take it out, commit, put it back, etc. So I just decided I'd put it in here behind a flag. --- bin/porting | 270 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100755 bin/porting diff --git a/bin/porting b/bin/porting new file mode 100755 index 0000000000000000000000000000000000000000..8313e6b881caeb8cd8ce5ab011e01e51dd54d7e9 --- /dev/null +++ b/bin/porting @@ -0,0 +1,270 @@ +#!/usr/bin/ruby +# frozen_string_literal: true + +require "date" +require "dhall" +require "em-hiredis" +require "em-http" +require "em_promise" +require "json" +require "optparse" +require "ruby-bandwidth-iris" +require "securerandom" +require "sentry-ruby" +require "time" + +@verbosity = 0 +@real_data = true +@dry_run = false + +OptionParser.new do |opts| + opts.banner = "Usage: porting [-vvf] DHALL_CONFIG" + + opts.on( + "-v", "--verbose", + "Print to terminal, run twice to not even send to customer" + ) do + @verbosity += 1 + end + + opts.on("-f", "--fake", "Run with fake ports rather than fetching") do + @real_data = false + end + + opts.on( + "-n", "--dry-run", + "Figure out what state they're in, but don't take action" + ) do + @dry_run = true + end + + opts.on("-h", "--help", "Print this help") do + puts opts + exit + end +end.parse! + +SCHEMA = "{ + bandwidth : { account: Text, username: Text, password: Text }, + xmpp: { jid: Text, password: Text }, + notification: { endpoint: Text, source_number: Text }, + pubsub: { server: Text, node: Text }, + testing_tel: Text, + admin_server: Text +}" + +raise "Need a Dhall config" unless ARGV[0] + +CONFIG = Dhall::Coder + .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc]) + .load("#{ARGV.first} : #{SCHEMA}", transform_keys: :to_sym) + +require_relative "../lib/blather_notify" +require_relative "../lib/expiring_lock" +require_relative "../lib/form_to_h" +require_relative "../lib/porting_step" + +Faraday.default_adapter = :em_synchrony +BandwidthIris::Client.global_options = { + account_id: CONFIG[:bandwidth][:account], + username: CONFIG[:bandwidth][:username], + password: CONFIG[:bandwidth][:password] +} + +class FullManual < PortingStepRepo::Outputs + def info(id, key, msg) + puts "[#{id}] INFO(#{key}): #{msg}" + end + + def warn(id, key, msg) + puts "[#{id}] WARN(#{key}): #{msg}" + end + + def error(id, key, e_or_msg) + puts "[#{id}] ERRR(#{key}): #{e_or_msg}" + return unless e_or_msg.respond_to?(:backtrace) + + e_or_msg.backtrace.each do |b| + puts "[#{id}] ERRR(#{key}): #{b}" + end + end + + def to_customer(id, key, tel, msg) + puts "[#{id}] CUST(#{key}, #{tel}): #{msg}" + end +end + +class ObservedAuto < FullManual + def initialize(endpoint, source_number) + @endpoint = endpoint + @src = source_number + end + + def to_customer(id, key, tel, msg) + ExpiringLock.new(lock_key(id, key)).with do + EM::HttpRequest + .new(@endpoint) + .apost( + head: { "Content-Type" => "application/json" }, + body: format_msg(tel, msg) + ) + end + end + +protected + + def lock_key(id, key) + "jmp_port_customer_msg_#{key}-#{id}" + end + + def format_msg(tel, msg) + [{ + time: DateTime.now.iso8601, + type: "message-received", + to: tel, + description: "Incoming message received", + message: actual_message(tel, msg) + }].to_json + end + + def actual_message(tel, msg) + { + id: SecureRandom.uuid, + owner: tel, + applicationId: SecureRandom.uuid, + time: DateTime.now.iso8601, + segmentCount: 1, + direction: "in", + to: [tel], from: @src, + text: msg + } + end +end + +class FullAutomatic < ObservedAuto + using FormToH + + def initialize(pubsub_addr, endpoint, source_number) + @pubsub = BlatherNotify.pubsub(pubsub_addr) + + Sentry.init do |config| + config.background_worker_threads = 0 + end + + super(endpoint, source_number) + end + + # No one's watch; swallow informational messages + def info(*); end + + def warn(id, key, msg) + ExpiringLock.new(warn_lock_key(id, key), expiry: 60 * 15).with do + entrykey = "#{id}:#{key}" + @pubsub.publish("#{entrykey}": error_entry("Port Warning", msg, entrykey)) + end + end + + def error(id, key, e_or_msg) + Sentry.with_scope do |scope| + scope.set_context("port", { id: id, action: key }) + + if e_or_msg.is_a?(::Exception) + Sentry.capture_exception(e_or_msg) + else + Sentry.capture_message(e_or_msg.to_s) + end + end + end + +protected + + def error_entry(title, text, id) + Nokogiri::XML::Builder.new { |xml| + xml.entry(xmlns: "http://www.w3.org/2005/Atom") do + xml.updated DateTime.now.iso8601 + xml.id id + xml.title title + xml.content text.to_s, type: "text" + xml.author { xml.name "porting" } + xml.generator "porting", version: "1.0" + end + }.doc.root + end + + def warn_lock_key(id, key) + "jmp_port_warn_msg_#{key}-#{id}" + end +end + +@output = + case @verbosity + when 0 + FullAutomatic.new( + BlatherNotify::PubSub::Address.new(**CONFIG[:pubsub]), + CONFIG[:notification][:endpoint], + CONFIG[:notification][:source_number] + ) + when 1 + ObservedAuto.new( + CONFIG[:notification][:endpoint], + CONFIG[:notification][:source_number] + ) + else + FullManual.new + end + +ports = if @real_data + BandwidthIris::PortIn.list( + page: 1, + size: 50, + start_date: Date.today - 1, + end_date: Date.today + ) || [] +else + MP = Struct.new( + :order_id, + :processing_status, + :actual_foc_date, + :last_modified_date, + :customer_order_id, + :billing_telephone_number + ) + + minutes = 1.0 / (24 * 60) + + [ + # This should be ignored + MP.new("T01", "SUBMITTED", nil, DateTime.now - 1, "ignored", "9998887777"), + MP.new( + "T02", "COMPLETE", DateTime.now - 60 * minutes, + DateTime.now - 55 * minutes, "0001", "2223334444" + ) + ] +end + +EM.run do + REDIS = EM::Hiredis.connect + + BlatherNotify.start( + CONFIG[:xmpp][:jid], + CONFIG[:xmpp][:password] + ).then { + ports.reduce(EMPromise.resolve(nil)) { |promise, port| + promise.then do + @output.info(port.order_id, :start, "Here we go") + PortingStepRepo.new(output: @output).find(port).then { |s| + @output.info(port.order_id, :class, s.class) + s + }.then { |s| + if @dry_run + @output.info("DRY", :dry, "Not taking action") + else + s.perform_next_step + end + } + end + } + }.catch { |e| + @output.error("ROOT", :catch, e) + }.then { BlatherNotify.shutdown } +end