hive-runner 2.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0a9df8820b8d030fa96b76d253723ccae821feb1
4
+ data.tar.gz: 7c2b1f9e1f75d269652e7e996ca515a135b92349
5
+ SHA512:
6
+ metadata.gz: 9e1d1ebb42336cc59d7f5f37fe98392887cadce6c23ef9787c4b4b7767e26a74f8adcfc40f23bd56eb0ea8c8b3a242a2aa5d215ac5dcd140a77fd534b70c61bb
7
+ data.tar.gz: cbc8e222372513f478cf809dc297e2b1b04511e48b00b9e498dc03d2cb5f72dca396c5cf4c57f49982b7a2697fdc59ad6c05ec388838d64e177c2cb1ad2c8c7b
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # hive-runner
2
+
3
+ Run automated jobs on devices
4
+
5
+ ## Quick start
6
+
7
+ Install the hive-runner gem and set up your hive:
8
+
9
+ gem install hive-runner
10
+ hive_setup my_hive
11
+
12
+ Follow the configuration instructions and, in particular, ensure that the
13
+ `HIVE_CONFIG` variable is set.
14
+
15
+ Start the Hive daemon:
16
+
17
+ hived start
18
+
19
+ Determine the status of the Hive:
20
+
21
+ hived status
22
+
23
+ Stop the Hive:
24
+
25
+ hived stop
26
+
27
+ ## Configuration file
28
+
29
+ Example config file:
30
+
31
+ test:
32
+ controllers:
33
+ shell:
34
+ max_workers: 5
35
+ name_stub: SHELL_WORKER
36
+ queues:
37
+ - bash
38
+
39
+ logging:
40
+ directory: logs
41
+ pids: pids
42
+ main_filename: hive.log
43
+
44
+ timings:
45
+ worker_loop_interval: 5
46
+
47
+ ### Controllers
48
+
49
+ The `controllers` section contains details about the controllers to be
50
+ used by the hive. The name of each section indicates the controller type. Some
51
+ of the fields in each controllers section is common to all controller types
52
+ (see below) while some are defined for each specific controller type.
53
+
54
+ Fields for all controller types are:
55
+
56
+ | Field | Content |
57
+ |-------------------|-------------------------------------------|
58
+ | `max_workers` | Maximum number of workers to use |
59
+ | `port_range_size` | Number of ports to allocate to the device |
60
+ | `name_stub` | Stub for name of the worker process |
61
+
62
+ ### Ports
63
+
64
+ | Field | Content |
65
+ |-----------|---------------------|
66
+ | `minimum` | Minimum port number |
67
+ | `maximum` | Maximum port number |
68
+
69
+ ### Logging
70
+
71
+ | Field | Content |
72
+ |-----------------|---------------------------|
73
+ | `directory` | Log file directory |
74
+ | `pids` | PIDs directory |
75
+ | `main_filename` | Name of the main log file |
76
+
77
+ ### Timings
78
+
79
+ The `worker_loop_interval` indicates the number of seconds to wait between each
80
+ poll of the job queue in the worker loop.
81
+
82
+ ## Shell controller
83
+
84
+ ### Configuration
85
+
86
+ The shell controller section contains the following additional field:
87
+
88
+ | Field | Content |
89
+ |-----------|-------------------------------------------|
90
+ | `queues` | Array of job queues for the shell workers |
91
+ | `workers` | Number of shell workers to run |
92
+
93
+ ## Setting up a new Hive Runner
94
+
95
+ Check out the Hive Runner from Github:
96
+
97
+ # Using HTTPS
98
+ git clone https://github.com/bbc-test/hive-runner
99
+ # ... or using SSH
100
+ git clone ssh@github.com:bbc-test/hive-runner
101
+ # Ensure ruby gems are installed
102
+ cd hive-runner
103
+ bundle install
104
+
105
+ Configure the hive, either by editing the default configuration file,
106
+ `hive-runner/config/hive-runner.yml`, or creating a separate configuration
107
+ file in a separate location (recommended) and ensuring that the `HIVE_CONFIG`
108
+ environment variable is set correctly:
109
+
110
+ echo export HIVE_CONFIG=/path/to/hive-config-directory >> ~/.bashrc
111
+
112
+ See the "Configuration file" above for details.
113
+
114
+ Start the Hive Runner:
115
+
116
+ ./bin/hived start
117
+
118
+ Ensure that your Hive Runner is running and that your workers have started:
119
+
120
+ ./bin/hived status
data/bin/hive_setup ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'fileutils'
5
+ require 'terminal-table'
6
+
7
+ if ARGV.length < 1
8
+ puts "Usage:"
9
+ puts
10
+ puts " hive_setup <directory>"
11
+ exit 1
12
+ end
13
+
14
+ dir = File.expand_path('', ARGV[0])
15
+ if Dir.exists?(dir)
16
+ if ! Dir["#{dir}/*"].empty?
17
+ puts "Directory '#{dir}' exists and is not empty"
18
+ exit 1
19
+ end
20
+ else
21
+ if File.exists?(dir)
22
+ puts "'#{dir}' exists and is not a directory"
23
+ exit 1
24
+ end
25
+ FileUtils.mkdir_p(dir)
26
+ end
27
+
28
+ def yn question
29
+ yn = 'x'
30
+ while yn !~ /^[yYnN]/
31
+ print "#{question}(y/n) "
32
+ yn = STDIN.gets.chomp
33
+ end
34
+ yn =~ /^[yY]/
35
+ end
36
+
37
+ # Choose options
38
+ opt = ''
39
+ mods = []
40
+ while opt.upcase != 'X'
41
+ table = Terminal::Table.new headings: ['Device', 'Module', 'Source']
42
+ mods.each do |mod|
43
+ table.add_row [
44
+ mod[:device],
45
+ mod[:name],
46
+ mod.has_key?(:git_account) ?
47
+ "git@github.com:#{mod[:git_account]}/#{mod[:name]}" :
48
+ "https://rubygems.org/gems/#{mod[:name]}"
49
+ ]
50
+ end
51
+
52
+ puts ''
53
+ puts table
54
+ puts ''
55
+ puts '1) Add module'
56
+ puts 'X) Continue'
57
+ puts ''
58
+ print "> "
59
+ opt = STDIN.gets.chomp
60
+
61
+ case opt
62
+ when '1'
63
+ mod = {}
64
+ puts ''
65
+ print "Module name: "
66
+ mod[:device] = STDIN.gets.chomp
67
+ mod[:name] = "hive-runner-#{mod[:device]}"
68
+ puts ''
69
+ if yn "Get '#{mod[:name]}' from Github? "
70
+ print "Enter GIT account name: "
71
+ mod[:git_account] = STDIN.gets.chomp
72
+ end
73
+
74
+ puts ''
75
+ puts "Module '#{mod[:name]}'"
76
+ if mod.has_key?(:git_account)
77
+ puts " from git@github.com:#{mod[:git_account]}/#{mod[:name]}"
78
+ else
79
+ puts " from https://rubygems.org/gems/#{mod[:name]}"
80
+ end
81
+ puts ''
82
+
83
+ if yn "Correct? "
84
+ mods << mod
85
+ end
86
+ end
87
+ end
88
+
89
+ FileUtils.mkdir_p("#{dir}/config")
90
+ FileUtils.mkdir_p("#{dir}/log")
91
+ FileUtils.mkdir_p("#{dir}/pids")
92
+ FileUtils.mkdir_p("#{dir}/workspaces")
93
+
94
+ File.open("#{dir}/config/settings.yml", 'w') do |f|
95
+ f.puts "#{ENV['HIVE_ENVIRONMENT'] || 'test'}:"
96
+ f.puts ' daemon_name: HIVE'
97
+ f.puts ''
98
+ f.puts ' controllers:'
99
+ f.puts ' shell:'
100
+ f.puts ' # Number of shell workers to allocate'
101
+ f.puts ' workers: 5'
102
+ f.puts ' # Queue for each shell worker'
103
+ f.puts ' queues:'
104
+ f.puts ' - bash'
105
+ f.puts ' # Number of ports to allocate to each shell worker'
106
+ f.puts ' port_range_size: 50'
107
+ f.puts ' name_stub: SHELL_WORKER'
108
+ mods.each do |m|
109
+ f.puts " #{m[:device]}:"
110
+ f.puts ' # Number of ports to allocate to each #{m[:device]} worker'
111
+ f.puts ' port_range_size: 50'
112
+ f.puts " name_stub: #{m[:device].upcase}_WORKER"
113
+ end
114
+ f.puts ''
115
+ f.puts ' # Range of ports to be made available to workers'
116
+ f.puts ' ports:'
117
+ f.puts ' minimum: 4000'
118
+ f.puts ' maximum: 5000'
119
+ f.puts ''
120
+ f.puts ' # Logging configuration'
121
+ f.puts ' logging:'
122
+ f.puts " directory: #{dir}/log"
123
+ f.puts " pids: #{dir}/pids"
124
+ f.puts ' main_filename: hive.log'
125
+ f.puts ' main_level: INFO'
126
+ f.puts ' worker_level: INFO'
127
+ f.puts " home: #{dir}/workspaces"
128
+ f.puts ' homes_to_keep: 5'
129
+ f.puts ''
130
+ f.puts ' # Timing configuration'
131
+ f.puts ' timings:'
132
+ f.puts ' worker_loop_interval: 5'
133
+ f.puts ' controller_loop_interval: 5'
134
+ f.puts ''
135
+ f.puts ' # Configuration for various network options'
136
+ f.puts ' network:'
137
+ f.puts ' scheduler: https://scheduler'
138
+ f.puts ' devicedb: https://devicedb'
139
+ f.puts ' cert: /path/to/certificate.pem'
140
+ f.puts ' cafile: /path/to/certificate-authorities.pem'
141
+ f.puts ''
142
+ f.puts ' # Configuration for diagnostic plugins'
143
+ f.puts ' diagnostics:'
144
+ end
145
+
146
+ File.open("#{dir}/Gemfile", 'w') do |f|
147
+ f.puts "source 'https://rubygems.org/'"
148
+ f.puts ""
149
+ f.puts "gem 'hive-runner'"
150
+ mods.each do |m|
151
+ source = m.has_key?(:git_account) ? ", git: 'git@github.com:#{m[:git_account]}/#{m[:name]}'" : ''
152
+ f.puts "gem '#{m[:name]}'#{source}"
153
+ end
154
+ end
155
+
156
+ print "Executing 'bundle install' ... "
157
+ Dir.chdir dir
158
+ if system("bundle install > bundle_install.out 2>&1")
159
+ print "SUCCESS\n"
160
+ File.delete('bundle_install.out')
161
+ else
162
+ print "FAILED\n"
163
+ puts "See #{dir}/bundle_install.out for details"
164
+ exit
165
+ end
166
+
167
+ puts ''
168
+ puts 'Configuration required:'
169
+ puts
170
+ puts ' * Add to config/settings.yml'
171
+ puts ' - scheduler'
172
+ puts ' - devicedb'
173
+ puts ' - cert'
174
+ puts ' - cafile'
175
+ if mods.length > 0
176
+ puts ' * Configure these modules in config/settings.yml'
177
+ mods.each do |m|
178
+ puts " - #{m[:device]}"
179
+ end
180
+ end
181
+ puts ' * Add to ~/.bashrc'
182
+ puts " - export HIVE_CONFIG=#{dir}/config"
data/bin/hived ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path(
5
+ '../../Gemfile',
6
+ Pathname.new(__FILE__).realpath
7
+ )
8
+
9
+ require 'rubygems'
10
+ require 'bundler/setup'
11
+ require 'socket'
12
+ require 'daemons'
13
+
14
+ # require 'hive' here to cause a failure if the configuration is not correct
15
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
16
+ require 'hive'
17
+
18
+ def test(app)
19
+ puts ''
20
+ app.default_show_status
21
+
22
+ Process.kill('USR1', app.pid.pid)
23
+ puts
24
+ server = TCPSocket.open('localhost', ENV.fetch('HIVE_COMM_PORT', 9999).to_i)
25
+ while line = server.gets
26
+ puts line.chop
27
+ end
28
+ server.close
29
+ end
30
+
31
+ Daemons.run(File.expand_path('..', __FILE__) + '/start_hive',
32
+ show_status_callback: :test,
33
+ log_dir: Hive.config.logging.directory,
34
+ log_output: true,
35
+ output_logfilename: 'hived.out')
data/bin/start_hive ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ puts "*** Starting Hive at #{Time.now} ***"
4
+ require 'pathname'
5
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path(
6
+ '../../Gemfile',
7
+ Pathname.new(__FILE__).realpath
8
+ )
9
+
10
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
11
+
12
+ require 'socket'
13
+ require 'terminal-table'
14
+ require 'hive'
15
+
16
+ $PROGRAM_NAME = Hive::DAEMON_NAME
17
+
18
+ # Version information
19
+ gems = {}
20
+ Gem::Specification.find_all_by_name(/hive-*/).each do |g|
21
+ gems[g.name] = g.version
22
+ end
23
+
24
+ # Communication with daemon launcher
25
+ comm_port = ENV.fetch('HIVE_COMM_PORT', 9999).to_i
26
+ comm = TCPServer.open(comm_port)
27
+ Signal.trap('USR1') do
28
+ client = comm.accept
29
+ client.puts " Controllers in use:"
30
+ Hive.register.controllers.each do |c|
31
+ client.puts " * #{c.class.name.split('::').last.downcase}"
32
+ end
33
+
34
+ client.puts
35
+
36
+ client.puts " Software versions:"
37
+ client.puts " * hive-runner: #{gems['hive-runner']}"
38
+ gems.except('hive-runner').sort.each do |name, version|
39
+ client.puts " * #{name}: #{version}"
40
+ end
41
+
42
+ client.puts
43
+
44
+ # Create a hash of connected device details
45
+ devices = Hive.register.devices
46
+ device_details = {}
47
+ client.puts " Total number of devices: #{devices.length}"
48
+ if devices.length > 0
49
+ devices.each do |d|
50
+ device_details[d.identity] = { worker: d.claimed? ? 'Claimed' : d.worker_pid }
51
+ end
52
+ end
53
+ client.puts ''
54
+
55
+ # Collect up the queue information per device/worker
56
+ device_details.each do |d, details|
57
+ if pid = details[:worker]
58
+ begin
59
+ queue_file = "#{Hive.config.logging.directory}/#{pid}.queues.yml"
60
+ details[:queues] = YAML.load( File.open(queue_file) )
61
+ rescue
62
+ details[:queues] = [ "---" ]
63
+ end
64
+ end
65
+ end
66
+
67
+ # Get a seperate hash of workspace details
68
+ workspaces_list = {}
69
+ Dir["#{Hive.config.logging.home}/*"].each do |d|
70
+ if File.directory?(d)
71
+ if File.exists?("#{d}/job_info")
72
+ File.open("#{d}/job_info") do |f|
73
+ if f.read =~ /^(\d*)\s*(\S*)$/
74
+ worker = $1 || '?'
75
+ state = $2 || '?'
76
+ if workspaces_list.has_key?(worker)
77
+ workspaces_list[worker][File.basename(d)] = state
78
+ else
79
+ workspaces_list[worker] = {File.basename(d) => state}
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # Build the table from all that data just collected
88
+ table = Terminal::Table.new headings: ['Device', 'Worker', 'Job', 'Status', 'Queues']
89
+ device_details.each do |d, details|
90
+ table.add_separator
91
+ jobs = states = [ '---' ]
92
+ if workspaces_list.has_key?(details[:worker].to_s)
93
+ jobs = workspaces_list[details[:worker].to_s].keys
94
+ states = jobs.map { |key| workspaces_list[details[:worker].to_s][key] }
95
+ workspaces_list.delete(details[:worker].to_s)
96
+ end
97
+ table.add_row [d, details[:worker], jobs.join("\n"), states.join("\n"), details[:queues].join("\n")]
98
+ end
99
+ workspaces_list.each do |worker, list|
100
+ table.add_separator
101
+ col1 = '---'
102
+ col2 = worker
103
+ col5 = '---'
104
+ list.each do |job, state|
105
+ table.add_row [col1, col2, job, state, col5]
106
+ col1 = col2 = ''
107
+ end
108
+ end
109
+ client.puts table
110
+
111
+ client.close
112
+ end
113
+
114
+ # Initialise
115
+ Hive.register.instantiate_controllers
116
+
117
+ # Execution loop
118
+ Hive::logger.info('*** HIVE STARTING ***')
119
+ Hive::logger.info('Starting execution loop')
120
+ Hive.register.run
@@ -0,0 +1,23 @@
1
+ require 'hive/controller'
2
+ require 'hive/worker/shell'
3
+
4
+ module Hive
5
+ class Controller
6
+ # The Shell controller
7
+ class Shell < Controller
8
+ def initialize(options)
9
+ Hive.logger.debug("options: #{options.inspect}")
10
+ @workers = options['workers'] || 0
11
+ super
12
+ end
13
+
14
+ def detect
15
+ Hive.logger.info('Creating shell devices')
16
+ (1..@workers).collect do |i|
17
+ Hive.logger.info(" Shell device #{i}")
18
+ self.create_device('id' => i)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ require 'hive'
2
+
3
+ module Hive
4
+ # Generic hive controller class
5
+ class Controller
6
+ attr_reader :port_range_size
7
+
8
+ class DeviceDetectionFailed < StandardError
9
+ end
10
+
11
+ class NoPortsAvailable < StandardError
12
+ end
13
+
14
+ def initialize(config = {})
15
+ @config = config
16
+ @device_class = self.class.to_s.sub('Controller', 'Device')
17
+ require @device_class.downcase.gsub(/::/, '/')
18
+ Hive.logger.info("Controller '#{self.class}' created")
19
+ @port_range_size = (@config.has_key?('port_range_size') ? @config['port_range_size'] : 0)
20
+ end
21
+
22
+ def create_device(extra_options = {})
23
+ object = Object
24
+ @device_class.split('::').each { |sub| object = object.const_get(sub) }
25
+ object.new(@config.merge(extra_options))
26
+ end
27
+
28
+ def detect
29
+ raise NotImplementedError, "'detect' method not defined for '#{self.class}'"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ require 'hive/device'
2
+
3
+ module Hive
4
+ class Device
5
+ # The Shell device
6
+ class Shell < Device
7
+ def initialize(config)
8
+ Hive.logger.info(" In the shell device constructor")
9
+ @identity = config['id']
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,88 @@
1
+ require 'hive'
2
+ require 'hive/port_allocator'
3
+
4
+ module Hive
5
+ # The generic device class
6
+ class Device
7
+ attr_reader :type
8
+ attr_accessor :status
9
+ attr_accessor :port_allocator
10
+
11
+ # Initialise the device
12
+ def initialize(options)
13
+ @worker_pid = nil
14
+ @options = options
15
+ @port_allocator = options['port_allocator'] or Hive::PortAllocator.new(ports: [])
16
+ @status = @options.has_key?('status') ? @options['status'] : 'none'
17
+ @worker_class = self.class.to_s.sub('Device', 'Worker')
18
+ require @worker_class.downcase.gsub(/::/, '/')
19
+ raise ArgumentError, "Identity not set for #{self.class} device" if ! @identity
20
+ end
21
+
22
+ # Start the worker process
23
+ def start
24
+ parent_pid = Process.pid
25
+ @worker_pid = Process.fork do
26
+ object = Object
27
+ @worker_class.split('::').each { |sub| object = object.const_get(sub) }
28
+ object.new(@options.merge('parent_pid' => parent_pid, 'device_identity' => self.identity, 'port_allocator' => self.port_allocator))
29
+ end
30
+ Process.detach @worker_pid
31
+
32
+ Hive.logger.info("Worker started with pid #{@worker_pid}")
33
+ end
34
+
35
+ # Terminate the worker process
36
+ def stop
37
+ begin
38
+ count = 0
39
+ while self.running? && count < 30 do
40
+ count += 1
41
+ Hive.logger.info("Attempting to terminate process #{@worker_pid} [#{count}]")
42
+ Process.kill 'TERM', @worker_pid
43
+ sleep 30
44
+ end
45
+ Process.kill 'KILL', @worker_pid if self.running?
46
+ rescue => e
47
+ Hive.logger.info("Process had already terminated")
48
+ end
49
+ @worker_pid = nil
50
+ end
51
+
52
+ # Test the state of the worker process
53
+ def running?
54
+ if @worker_pid
55
+ begin
56
+ Process.kill 0, @worker_pid
57
+ true
58
+ rescue Errno::ESRCH
59
+ false
60
+ end
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ # Return the worker pid, checking to see if it is running first
67
+ def worker_pid
68
+ @worker_pid = nil if ! self.running?
69
+ @worker_pid
70
+ end
71
+
72
+ # Return true if the device is claimed
73
+ # If the device has no status set it is assumed not to be claimed
74
+ def claimed?
75
+ @status == 'claimed'
76
+ end
77
+
78
+ # Test equality with another device
79
+ def ==(other)
80
+ self.identity == other.identity
81
+ end
82
+
83
+ # Return the unique identity of the device
84
+ def identity
85
+ "#{self.class.to_s.split('::').last}-#{@identity}"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,54 @@
1
+ require 'hive'
2
+ require 'hive/results'
3
+
4
+ module Hive
5
+ class Diagnostic
6
+
7
+ class InvalidParameterError < StandardError
8
+ end
9
+
10
+ attr_accessor :last_run
11
+ attr_reader :config, :device_api
12
+
13
+ def initialize(config, options)
14
+ @options = options
15
+ @config = config
16
+ @serial = @options['serial']
17
+ @device_api = @options['device_api']
18
+ end
19
+
20
+ def should_run?
21
+ return true if @last_run == nil
22
+ time_now = Time.new.getutc
23
+ last_run_time = @last_run.timestamp
24
+ diff = ((time_now - last_run_time)/300).round
25
+ if (diff > 2 && @last_run.passed?) || diff > 1
26
+ true
27
+ else
28
+ false
29
+ end
30
+ end
31
+
32
+ def run
33
+ Hive.logger.info("Trying to run diagnostic '#{self.class}'")
34
+ if should_run?
35
+ result = diagnose
36
+ result = repair(result) if result.failed?
37
+ @last_run = result
38
+ else
39
+ Hive.logger.info("Diagnostic '#{self.class}' last ran less than five minutes before")
40
+ end
41
+ @last_run
42
+ end
43
+
44
+ def pass(message= {}, data = {})
45
+ Hive.logger.info(message)
46
+ Hive::Results.new("pass", message, data )
47
+ end
48
+
49
+ def fail(message ={}, data = {})
50
+ Hive.logger.info(message)
51
+ Hive::Results.new("fail", message, data)
52
+ end
53
+ end
54
+ end