solr_makr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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