hive-runner 2.0.0

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