scenario_import.rb 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. require 'ostruct'
  2. # This is a helper class for managing Scenario imports, used by the ScenarioImportsController. This class behaves much
  3. # like a normal ActiveRecord object, with validations and callbacks. However, it is never persisted to the database.
  4. class ScenarioImport
  5. include ActiveModel::Model
  6. include ActiveModel::Callbacks
  7. include ActiveModel::Validations::Callbacks
  8. DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent]
  9. URL_REGEX = /\Ahttps?:\/\//i
  10. attr_accessor :file, :url, :data, :do_import, :merges
  11. attr_reader :user
  12. before_validation :parse_file
  13. before_validation :fetch_url
  14. validate :validate_presence_of_file_url_or_data
  15. validates_format_of :url, :with => URL_REGEX, :allow_nil => true, :allow_blank => true, :message => "appears to be invalid"
  16. validate :validate_data
  17. validate :generate_diff
  18. def step_one?
  19. data.blank?
  20. end
  21. def step_two?
  22. data.present?
  23. end
  24. def set_user(user)
  25. @user = user
  26. end
  27. def existing_scenario
  28. @existing_scenario ||= user.scenarios.find_by(:guid => parsed_data["guid"])
  29. end
  30. def dangerous?
  31. (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) }
  32. end
  33. def parsed_data
  34. @parsed_data ||= (data && JSON.parse(data) rescue {}) || {}
  35. end
  36. def agent_diffs
  37. @agent_diffs || generate_diff
  38. end
  39. def should_import?
  40. do_import == "1"
  41. end
  42. def import(options = {})
  43. success = true
  44. guid = parsed_data['guid']
  45. description = parsed_data['description']
  46. name = parsed_data['name']
  47. links = parsed_data['links']
  48. tag_fg_color = parsed_data['tag_fg_color']
  49. tag_bg_color = parsed_data['tag_bg_color']
  50. source_url = parsed_data['source_url'].presence || nil
  51. @scenario = user.scenarios.where(:guid => guid).first_or_initialize
  52. @scenario.update_attributes!(:name => name, :description => description,
  53. :source_url => source_url, :public => false,
  54. :tag_fg_color => tag_fg_color,
  55. :tag_bg_color => tag_bg_color)
  56. unless options[:skip_agents]
  57. created_agents = agent_diffs.map do |agent_diff|
  58. agent = agent_diff.agent || Agent.build_for_type("Agents::" + agent_diff.type.incoming, user)
  59. agent.guid = agent_diff.guid.incoming
  60. agent.attributes = { :name => agent_diff.name.updated,
  61. :disabled => agent_diff.disabled.updated, # == "true"
  62. :options => agent_diff.options.updated,
  63. :scenario_ids => [@scenario.id] }
  64. agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present?
  65. agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present?
  66. agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true"
  67. agent.service_id = agent_diff.service_id.updated if agent_diff.service_id.present?
  68. unless agent.save
  69. success = false
  70. errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}")
  71. end
  72. agent
  73. end
  74. if success
  75. links.each do |link|
  76. receiver = created_agents[link['receiver']]
  77. source = created_agents[link['source']]
  78. receiver.sources << source unless receiver.sources.include?(source)
  79. end
  80. end
  81. end
  82. success
  83. end
  84. def scenario
  85. @scenario || @existing_scenario
  86. end
  87. def will_request_local?(url_root)
  88. data.blank? && file.blank? && url.present? && url.starts_with?(url_root)
  89. end
  90. protected
  91. def parse_file
  92. if data.blank? && file.present?
  93. self.data = file.read.force_encoding(Encoding::UTF_8)
  94. end
  95. end
  96. def fetch_url
  97. if data.blank? && url.present? && url =~ URL_REGEX
  98. self.data = Faraday.get(url).body
  99. end
  100. end
  101. def validate_data
  102. if data.present?
  103. @parsed_data = JSON.parse(data) rescue {}
  104. if (%w[name guid agents] - @parsed_data.keys).length > 0
  105. errors.add(:base, "The provided data does not appear to be a valid Scenario.")
  106. self.data = nil
  107. end
  108. else
  109. @parsed_data = nil
  110. end
  111. end
  112. def validate_presence_of_file_url_or_data
  113. unless file.present? || url.present? || data.present?
  114. errors.add(:base, "Please provide either a Scenario JSON File or a Public Scenario URL.")
  115. end
  116. end
  117. def generate_diff
  118. @agent_diffs = (parsed_data['agents'] || []).map.with_index do |agent_data, index|
  119. # AgentDiff is defined at the end of this file.
  120. agent_diff = AgentDiff.new(agent_data, parsed_data['schema_version'])
  121. if existing_scenario
  122. # If this Agent exists already, update the AgentDiff with the local version's information.
  123. agent_diff.diff_with! existing_scenario.agents.find_by(:guid => agent_data['guid'])
  124. begin
  125. # Update the AgentDiff with any hand-merged changes coming from the UI. This only happens when this
  126. # Agent already exists locally and has conflicting changes.
  127. agent_diff.update_from! merges[index.to_s] if merges
  128. rescue JSON::ParserError
  129. errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.")
  130. end
  131. end
  132. if agent_diff.requires_service? && merges.present? && merges[index.to_s].present? && merges[index.to_s]['service_id'].present?
  133. agent_diff.service_id = AgentDiff::FieldDiff.new(merges[index.to_s]['service_id'].to_i)
  134. end
  135. agent_diff
  136. end
  137. end
  138. # AgentDiff is a helper object that encapsulates an incoming Agent. All fields will be returned as an array
  139. # of either one or two values. The first value is the incoming value, the second is the existing value, if
  140. # it differs from the incoming value.
  141. class AgentDiff < OpenStruct
  142. class FieldDiff
  143. attr_accessor :incoming, :current, :updated
  144. def initialize(incoming)
  145. @incoming = incoming
  146. @updated = incoming
  147. end
  148. def set_current(current)
  149. @current = current
  150. @requires_merge = (incoming != current)
  151. end
  152. def requires_merge?
  153. @requires_merge
  154. end
  155. end
  156. def initialize(agent_data, schema_version)
  157. super()
  158. @schema_version = schema_version
  159. @requires_merge = false
  160. self.agent = nil
  161. store! agent_data
  162. end
  163. BASE_FIELDS = %w[name schedule keep_events_for propagate_immediately disabled guid]
  164. FIELDS_REQUIRING_TRANSLATION = %w[keep_events_for]
  165. def agent_exists?
  166. !!agent
  167. end
  168. def requires_merge?
  169. @requires_merge
  170. end
  171. def requires_service?
  172. !!agent_instance.try(:oauthable?)
  173. end
  174. def store!(agent_data)
  175. self.type = FieldDiff.new(agent_data["type"].split("::").pop)
  176. self.options = FieldDiff.new(agent_data['options'] || {})
  177. BASE_FIELDS.each do |option|
  178. if agent_data.has_key?(option)
  179. value = agent_data[option]
  180. value = send(:"translate_#{option}", value) if option.in?(FIELDS_REQUIRING_TRANSLATION)
  181. self[option] = FieldDiff.new(value)
  182. end
  183. end
  184. end
  185. def translate_keep_events_for(old_value)
  186. if schema_version < 1
  187. # Was stored in days, now is stored in seconds.
  188. old_value.to_i.days
  189. else
  190. old_value
  191. end
  192. end
  193. def schema_version
  194. (@schema_version || 0).to_i
  195. end
  196. def diff_with!(agent)
  197. return unless agent.present?
  198. self.agent = agent
  199. type.set_current(agent.short_type)
  200. options.set_current(agent.options || {})
  201. @requires_merge ||= type.requires_merge?
  202. @requires_merge ||= options.requires_merge?
  203. BASE_FIELDS.each do |field|
  204. next unless self[field].present?
  205. self[field].set_current(agent.send(field))
  206. @requires_merge ||= self[field].requires_merge?
  207. end
  208. end
  209. def update_from!(merges)
  210. each_field do |field, value, selection_options|
  211. value.updated = merges[field]
  212. end
  213. if options.requires_merge?
  214. options.updated = JSON.parse(merges['options'])
  215. end
  216. end
  217. def each_field
  218. boolean = [["True", "true"], ["False", "false"]]
  219. yield 'name', name if name.requires_merge?
  220. yield 'schedule', schedule, Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] } if self['schedule'].present? && schedule.requires_merge?
  221. yield 'keep_events_for', keep_events_for, Agent::EVENT_RETENTION_SCHEDULES if self['keep_events_for'].present? && keep_events_for.requires_merge?
  222. yield 'propagate_immediately', propagate_immediately, boolean if self['propagate_immediately'].present? && propagate_immediately.requires_merge?
  223. yield 'disabled', disabled, boolean if disabled.requires_merge?
  224. end
  225. # Unfortunately Ruby 1.9's OpenStruct doesn't expose [] and []=.
  226. unless instance_methods.include?(:[]=)
  227. def [](key)
  228. self.send(sanitize key)
  229. end
  230. def []=(key, val)
  231. self.send("#{sanitize key}=", val)
  232. end
  233. def sanitize(key)
  234. key.gsub(/[^a-zA-Z0-9_-]/, '')
  235. end
  236. end
  237. def agent_instance
  238. "Agents::#{self.type.updated}".constantize.new
  239. end
  240. end
  241. end