solr_makr 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+
4
+ class Pathname
5
+ alias_method :to_str, :to_s
6
+ end
7
+
8
+ require "active_support/concern"
9
+ require "active_support/configurable"
10
+ require "active_support/core_ext/object/blank"
11
+ require "active_support/core_ext/object/try"
12
+ require "active_support/core_ext/module/delegation"
13
+ require "attr_lazy"
14
+ require "commander"
15
+ require "nokogiri"
16
+ require "typhoeus"
17
+ require "virtus"
18
+
19
+ require "solr_makr/version"
20
+ require "solr_makr/solr_request"
21
+ require "solr_makr/core_status"
22
+ require "solr_makr/solr_configuration"
23
+ require "solr_makr/core"
24
+ require "solr_makr/commands"
25
+ require "solr_makr/commands/shared"
26
+ require "solr_makr/commands/create_core"
27
+ require "solr_makr/commands/destroy_core"
28
+ require "solr_makr/commands/list_cores"
29
+ require "solr_makr/application"
30
+
31
+ module SolrMakr
32
+ class << self
33
+ TEMPLATE_PATH = Pathname.new(File.dirname(__FILE__)).join('files')
34
+
35
+ # @param [String] path
36
+ # @raise [Errno::ENOENT] if undefined template
37
+ # @return [String] contents of file
38
+ def template(path)
39
+ TEMPLATE_PATH.join(path).read
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ module SolrMakr
2
+ class Application
3
+ include Commander::Methods
4
+
5
+ NAME = 'solr-makr'
6
+
7
+ def run
8
+ program :name, NAME
9
+ program :version, SolrMakr::VERSION
10
+ program :description, 'Create a solr core programmatically'
11
+ program :help, 'Author', 'Alexa Grey <alexag@hranswerlink.com>'
12
+ program :help_formatter, Commander::HelpFormatter::TerminalCompact
13
+
14
+ default_command :help
15
+
16
+ global_option '-d', '--solr-home DIR', 'Path to the solr home directory'
17
+ global_option '-p', '--solr-port PORT', Integer, 'Port to use to communicate with the solr API'
18
+ global_option '-V', '--verbose', 'Show verbose output.'
19
+
20
+ command :create do |c|
21
+ c.syntax = "#{NAME} create NAME"
22
+
23
+ c.description = "Create and register a solr core."
24
+
25
+ c.when_called Commands::CreateCore, :run!
26
+ end
27
+
28
+ command :list do |c|
29
+ c.syntax = "#{NAME} list"
30
+
31
+ c.description = "List installed solr cores."
32
+
33
+ c.when_called Commands::ListCores, :run!
34
+ end
35
+
36
+ command :destroy do |c|
37
+ c.syntax = "#{NAME} destroy NAME"
38
+
39
+ c.option '--purge', 'Purge the solr core\'s instance directory'
40
+
41
+ c.description = "Unload and remove a solr core."
42
+
43
+ c.when_called Commands::DestroyCore, :run!
44
+ end
45
+
46
+ run!
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ module SolrMakr
2
+ # @api private
3
+ module Commands
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ module SolrMakr
2
+ module Commands
3
+ class CreateCore
4
+ include Shared
5
+
6
+ config.acts_on_single_core = true
7
+
8
+ def run
9
+ with_message "initializing configuration" do
10
+ core.initialize_configuration!
11
+ end
12
+
13
+ with_message "registering new core with solr" do
14
+ core.register_with_solr!
15
+ end
16
+
17
+ say "Created core: #{core_name}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module SolrMakr
2
+ module Commands
3
+ class DestroyCore
4
+ include Shared
5
+
6
+ config.acts_on_single_core = true
7
+
8
+ def run
9
+ with_message "registering new core with solr" do
10
+ core.unload_from_solr!(purge: options.purge)
11
+ end
12
+
13
+ say "Destroyed core: #{core_name}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module SolrMakr
2
+ module Commands
3
+ class ListCores
4
+ include Shared
5
+
6
+ def run
7
+ say "Listing core(s)."
8
+
9
+ cores = solr_config.solr_status
10
+
11
+ cores.each do |core|
12
+ say sprintf(" * %s -- %d document(s) / %s", core.name, core.documents, core.size)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,82 @@
1
+ module SolrMakr
2
+ module Commands
3
+ module Shared
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include ActiveSupport::Configurable
8
+
9
+ config.core_name_index = 0
10
+ end
11
+
12
+ delegate :name, allow_nil: true, to: :core, prefix: true
13
+
14
+ # @!attribute [r] solr_config
15
+ # @return [SolrMakr::SolrConfiguration]
16
+ attr_reader :solr_config
17
+
18
+ # @!attribute [r] args
19
+ # @return [<String>]
20
+ attr_reader :args
21
+
22
+ # @!attribute [r] options
23
+ # @return [Struct]
24
+ attr_reader :options
25
+
26
+ delegate :home, :port, prefix: :solr, to: :solr_config
27
+
28
+ alias_method :port, :solr_port
29
+
30
+ # @abstract
31
+ # @return [void]
32
+ def run
33
+ raise NotImplementedError, "Must implement #run"
34
+ end
35
+
36
+ # @return [void]
37
+ def run!(args, options)
38
+ @args = args
39
+ @options = options
40
+
41
+ options.default shared_options
42
+
43
+ solr_config.home = options.solr_home
44
+ solr_config.port = options.solr_port
45
+
46
+ run
47
+ end
48
+
49
+ # @!attribute [r] solr_config
50
+ # @return [SolrMakr::SolrConfiguration]
51
+ attr_lazy_reader :solr_config do
52
+ SolrMakr::SolrConfiguration.new
53
+ end
54
+
55
+ # @!attribute [r] core
56
+ # @return [SolrMakr::Core]
57
+ attr_lazy_reader :core do
58
+ core_name = args[config.core_name_index]
59
+
60
+ if core_name.present? || config.acts_on_single_core
61
+ solr_config.core(name: core_name)
62
+ end
63
+ end
64
+
65
+ def with_message(message, &block)
66
+ retval = yield
67
+
68
+ if retval != false && options.verbose
69
+ say message
70
+ end
71
+ end
72
+
73
+ private
74
+ def shared_options
75
+ {
76
+ solr_home: solr_home,
77
+ solr_port: solr_port
78
+ }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,92 @@
1
+ module SolrMakr
2
+ # @api private
3
+ class CoreName < Virtus::Attribute
4
+ NAME_FORMAT = /\A[\w\-]+\z/i
5
+
6
+ # @param [String] value
7
+ # @return [String]
8
+ def coerce(value)
9
+ value = value.to_s
10
+
11
+ raise 'Invalid Core Name' unless value.present?
12
+ raise 'Invalid Core Name Format' unless value =~ NAME_FORMAT
13
+
14
+ return value
15
+ end
16
+ end
17
+
18
+ class Core
19
+ include Virtus.model strict: true
20
+ include SolrRequest
21
+
22
+ CONF_PATH = 'conf'
23
+
24
+ CONF_FILES = %w[schema.xml solrconfig.xml]
25
+
26
+ attribute :name, CoreName
27
+ attribute :config, SolrConfiguration
28
+
29
+ delegate :core_directory, :port, to: :config
30
+ delegate :exist?, to: :instance_dir
31
+
32
+ # @return [Pathname]
33
+ def instance_dir
34
+ core_directory.join name
35
+ end
36
+
37
+ def conf_dir
38
+ instance_dir.join(CONF_PATH)
39
+ end
40
+
41
+ def initialize_configuration!
42
+ conf_dir.mkpath
43
+
44
+ CONF_FILES.each do |conf_file|
45
+ path = conf_dir.join(conf_file)
46
+
47
+ path.write(SolrMakr.template(conf_file))
48
+ end
49
+ end
50
+
51
+ # @return [void]
52
+ def register_with_solr!
53
+ params = {
54
+ action: 'CREATE',
55
+ name: name,
56
+ persist: 'true',
57
+ instanceDir: instance_dir.to_s,
58
+ loadOnStartup: 'true',
59
+ config: 'solrconfig.xml',
60
+ schema: 'schema.xml'
61
+ }
62
+
63
+ solr_request solr_cores_url, params: params
64
+ end
65
+
66
+ def reload!
67
+ params = {
68
+ action: 'RELOAD',
69
+ core: name
70
+ }
71
+
72
+ solr_request solr_cores_url, params: params
73
+ end
74
+
75
+ def unload_from_solr!(purge: false)
76
+ params = {
77
+ core: name,
78
+ action: 'UNLOAD',
79
+ deleteIndex: 'true',
80
+ deleteDataDir: 'true'
81
+ }
82
+
83
+ if purge
84
+ say "Deleting entire instance dir!!!"
85
+ params[:deleteInstanceDir] = 'true'
86
+ end
87
+
88
+ solr_request solr_cores_url, params: params
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,56 @@
1
+ module SolrMakr
2
+ class CoreStatus
3
+ include Virtus.model strict: true
4
+
5
+ attribute :name, String
6
+ attribute :start_time, Time
7
+ attribute :uptime, Integer, default: 0
8
+ attribute :default_core, Boolean, default: false
9
+ attribute :size, String, default: '0 bytes'
10
+ attribute :size_in_bytes, Integer, default: 0
11
+ attribute :documents, Integer, default: 0
12
+
13
+ class << self
14
+ LOOKS_TRUE = /\At(?:rue)?\z/i
15
+
16
+ NAME_XML = 'str[name="name"]'
17
+ DEFAULT_CORE_XML = 'bool[name="isDefaultCore"]'
18
+ SIZE_XML = 'str[name="size"]'
19
+ SIZE_IN_BYTES_XML = 'long[name="sizeInBytes"]'
20
+ START_TIME_XML = 'date[name="startTime"]'
21
+ UPTIME_XML = 'long[name="uptime"]'
22
+ DOCUMENTS_XML = 'int[name="numDocs"]'
23
+
24
+ # @return [SolrMakr::CoreStatus]
25
+ def from_xml(node)
26
+ params = {
27
+ name: text_at_css(node, NAME_XML),
28
+ size: text_at_css(node, SIZE_XML),
29
+ size_in_bytes: text_at_css(node, SIZE_IN_BYTES_XML),
30
+ documents: text_at_css(node, DOCUMENTS_XML),
31
+ uptime: text_at_css(node, UPTIME_XML)
32
+ }
33
+
34
+ params[:default_core] = text_at_css(node, DEFAULT_CORE_XML) do |res|
35
+ res.to_s.match(LOOKS_TRUE).present?
36
+ end
37
+
38
+ params[:start_time] = text_at_css(node, START_TIME_XML) do |res|
39
+ Time.parse res if res.present?
40
+ end
41
+
42
+ new params
43
+ end
44
+
45
+ def text_at_css(node, selector, &block)
46
+ result = node.at_css(selector).try(:text)
47
+
48
+ if block_given?
49
+ yield result
50
+ else
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,78 @@
1
+ module SolrMakr
2
+ # @api private
3
+ class Pathlike < Virtus::Attribute
4
+ # @param [String / Pathname] value
5
+ # @return [Pathname]
6
+ def coerce(value)
7
+ Pathname.new value
8
+ end
9
+ end
10
+
11
+ class SolrConfiguration
12
+ include Virtus.model strict: true
13
+ include SolrRequest
14
+
15
+ DEFAULT_HOME = '/opt/solr/jetty-solr/'
16
+ DEFAULT_PORT = 8983
17
+
18
+ attribute :home, Pathlike, default: :default_home
19
+ attribute :port, Integer, default: :default_port
20
+
21
+ def initialize(env = ENV)
22
+ @_env = env
23
+
24
+ super()
25
+ end
26
+
27
+ def env
28
+ @_env ||= ENV
29
+ end
30
+
31
+ def core(name: nil)
32
+ Core.new name: name, config: self
33
+ end
34
+
35
+ def cores
36
+ core_directory.children.each_with_object [] do |path, cores|
37
+ cores << core(name: path.basename) if looks_like_core_directory?(path)
38
+ end
39
+ end
40
+
41
+ # @return [String]
42
+ def core_directory
43
+ home.join 'solr'
44
+ end
45
+
46
+ # @return [<SolrMakr::CoreStatus>]
47
+ def solr_status
48
+ resp = solr_request solr_cores_url, params: { action: 'STATUS' }
49
+
50
+ parsed = Nokogiri::XML(resp.body)
51
+
52
+ statuses = parsed.at_css('lst[name="status"]').try(:children)
53
+
54
+ return [] unless statuses.present?
55
+
56
+ statuses.each_with_object [] do |status, list|
57
+ created = SolrMakr::CoreStatus.from_xml status# rescue nil
58
+
59
+ list << created if created.present?
60
+ end
61
+ end
62
+
63
+ private
64
+ def looks_like_core_directory?(path)
65
+ path.join('data').exist?
66
+ end
67
+
68
+ def default_port
69
+ Integer(env.fetch 'SOLR_PORT', DEFAULT_PORT)
70
+ rescue ArgumentError
71
+ DEFAULT_PORT
72
+ end
73
+
74
+ def default_home
75
+ Pathname.new env.fetch 'SOLR_HOME', DEFAULT_HOME
76
+ end
77
+ end
78
+ end