Skip to content

Commit be4cfd3

Browse files
authored
feat: PPT-2047,PPT-2048 driver integrity checker + refactoring driver_manager (#281)
1 parent 0677849 commit be4cfd3

8 files changed

Lines changed: 543 additions & 287 deletions

File tree

spec/driver_cleanup_spec.cr renamed to spec/driver_manager/driver_cleanup_spec.cr

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
require "./helper"
1+
require "../helper"
22

33
module PlaceOS::Core
44
describe DriverCleanup do
5-
it "get running drivers information in expected format" do
5+
it "should capture and retrieve stale drivers" do
66
_, driver, mod = setup
77
module_manager = module_manager_mock
88

@@ -21,11 +21,12 @@ module PlaceOS::Core
2121

2222
module_manager.local_processes.run_count.should eq(ProcessManager::Count.new(1, 1))
2323

24-
expected = ["drivers_place_private_helper_cce023_#{DriverCleanup.arch}"]
25-
running = DriverCleanup.running_drivers
26-
running.should eq(expected)
27-
local = Dir.new(DriverStore::BINARY_PATH).children
28-
running.should eq(expected)
24+
tracker = DriverCleanup::StaleProcessTracker.new(DriverStore::BINARY_PATH, REDIS_CLIENT)
25+
stale_list = tracker.update_and_find_stale(ENV["STALE_THRESHOLD_DAYS"]?.try &.to_i || 30)
26+
stale_list.size.should eq(0)
27+
driver_file = Path[DriverStore::BINARY_PATH, "drivers_place_private_helper_cce023a_#{Core::ARCH}"].to_s
28+
value = REDIS_CLIENT.hgetall(driver_file)
29+
value["last_executed_at"].to_i64.should be > 0
2930
end
3031
end
3132
end

src/core-app.cr

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,6 @@ Signal::TERM.trap &terminate
102102
# Wait for redis and postgres to be ready
103103
PlaceOS::Core.wait_for_resources
104104

105-
# Start cleaning un-used driver task
106-
PlaceOS::Core::DriverCleanup.start_cleanup
107-
108105
spawn do
109106
begin
110107
PlaceOS::Core.start_managers

src/placeos-core/driver_cleanup.cr

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/placeos-core/driver_manager.cr

Lines changed: 22 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -1,240 +1,9 @@
1-
require "uri"
2-
require "digest"
3-
require "connect-proxy"
41
require "placeos-models"
52
require "placeos-resource"
63
require "./module_manager"
4+
require "./driver_manager/**"
75

86
module PlaceOS::Core
9-
class DriverStore
10-
BINARY_PATH = ENV["PLACEOS_DRIVER_BINARIES"]?.presence || Path["./bin/drivers"].expand.to_s
11-
12-
protected getter binary_path : String
13-
14-
def initialize(@binary_path : String = BINARY_PATH)
15-
Dir.mkdir_p binary_path
16-
end
17-
18-
def compiled?(file_name : String, commit : String, branch : String, uri : String) : Bool
19-
Log.debug { {message: "Checking whether driver is compiled or not?", driver: file_name, commit: commit, branch: branch, repo: uri} }
20-
path = Path[binary_path, executable_name(file_name, commit)]
21-
return true if File.exists?(path)
22-
resp = BuildApi.compiled?(file_name, commit, branch, uri)
23-
return false unless resp.success?
24-
ret = fetch_binary(LinkData.from_json(resp.body)) rescue nil
25-
!ret.nil?
26-
end
27-
28-
def compile(file_name : String, url : String, commit : String, branch : String, force : Bool, username : String? = nil, password : String? = nil, fetch : Bool = true) : Result
29-
Log.info { {message: "Requesting build service to compile driver", driver_file: file_name, branch: branch, repository: url} }
30-
begin
31-
resp = BuildApi.compile(file_name, url, commit, branch, force, username, password)
32-
unless fetch
33-
return Result.new(success: true)
34-
end
35-
resp = resp.not_nil!
36-
unless resp.success?
37-
Log.error { {message: resp.body, status_code: resp.status_code, driver: file_name, commit: commit, branch: branch, force: force} }
38-
return Result.new(output: resp.body, name: file_name)
39-
end
40-
link = LinkData.from_json(resp.body)
41-
begin
42-
driver = fetch_binary(link)
43-
rescue ex
44-
return Result.new(output: ex.message.not_nil!, name: file_name)
45-
end
46-
Result.new(success: true, name: driver, path: binary_path)
47-
rescue ex
48-
msg = ex.message || "compiled returned no exception message"
49-
Log.error(exception: ex) { {message: msg, driver: file_name, commit: commit, branch: branch, force: force} }
50-
Result.new(output: msg, name: file_name)
51-
end
52-
end
53-
54-
def metadata(file_name : String, commit : String, branch : String, uri : String)
55-
resp = BuildApi.metadata(file_name, commit, branch, uri)
56-
return Result.new(success: true, output: resp.body.as(String)) if resp.success?
57-
Result.new(output: "Metadata not found. Server returned #{resp.status_code}")
58-
rescue ex
59-
Result.new(output: ex.message.not_nil!, name: file_name)
60-
end
61-
62-
def defaults(file_name : String, commit : String, branch : String, uri : String)
63-
resp = BuildApi.defaults(file_name, commit, branch, uri)
64-
return Result.new(success: true, output: resp.body.as(String)) if resp.success?
65-
Result.new(output: "Driver defaults not found. Server returned #{resp.status_code}")
66-
rescue ex
67-
Result.new(output: ex.message.not_nil!, name: file_name)
68-
end
69-
70-
def built?(file_name : String, commit : String, branch : String, uri : String) : String?
71-
return nil unless compiled?(file_name, commit, branch, uri)
72-
driver_binary_path(file_name, commit).to_s
73-
end
74-
75-
def driver_binary_path(file_name : String, commit : String)
76-
Path[binary_path, executable_name(file_name, commit)]
77-
end
78-
79-
def path(driver_file : String) : Path
80-
Path[binary_path, driver_file]
81-
end
82-
83-
def compiled_drivers : Array(String)
84-
Dir.children(binary_path)
85-
end
86-
87-
def executable_name(driver_source, commit)
88-
driver_source = driver_source.rchop(".cr").gsub(/\/|\./, "_")
89-
commit = commit[..6] if commit.size > 6
90-
{driver_source, commit, Core::ARCH}.join("_").downcase
91-
end
92-
93-
def reload_driver(driver_id : String)
94-
if driver = Model::Driver.find?(driver_id)
95-
repo = driver.repository!
96-
97-
if compiled?(driver.file_name, driver.commit, repo.branch, repo.uri)
98-
manager = ModuleManager.instance
99-
stale_path = manager.reload_modules(driver)
100-
if path = stale_path
101-
File.delete(path) rescue nil if File.exists?(path)
102-
end
103-
else
104-
return {status: 404, message: "Driver not compiled or not available on S3"}
105-
end
106-
else
107-
return {status: 404, message: "Driver with id #{driver_id} not found "}
108-
end
109-
{status: 200, message: "OK"}
110-
end
111-
112-
private def fetch_binary(link : LinkData) : String
113-
url = URI.parse(link.url)
114-
driver_file = Path[url.path].basename
115-
filename = Path[binary_path, driver_file]
116-
resp = if Core.production? || url.scheme == "https"
117-
ConnectProxy::HTTPClient.get(url.to_s)
118-
else
119-
uri = URI.new(path: url.path, query: url.query)
120-
ConnectProxy::HTTPClient.new(url.host.not_nil!, 9000).get(uri.to_s)
121-
end
122-
if resp.success?
123-
unless link.size == resp.headers.fetch("Content-Length", "0").to_i
124-
Log.error { {message: "Expected content length #{link.size}, but received #{resp.headers.fetch("Content-Length", "0")}", driver_file: driver_file} }
125-
raise Error.new("Response size doesn't match with build service returned result")
126-
end
127-
128-
body_io = IO::Digest.new(resp.body_io? || IO::Memory.new(resp.body), Digest::MD5.new)
129-
File.open(filename, "wb+") do |f|
130-
IO.copy(body_io, f)
131-
f.chmod(0o755)
132-
end
133-
filename.to_s
134-
else
135-
raise Error.new("Unable to fetch driver. Error : #{resp.body}")
136-
end
137-
end
138-
139-
private record LinkData, size : Int64, md5 : String, modified : Time, url : String, link_expiry : Time do
140-
include JSON::Serializable
141-
@[JSON::Field(converter: Time::EpochConverter)]
142-
getter modified : Time
143-
@[JSON::Field(converter: Time::EpochConverter)]
144-
getter link_expiry : Time
145-
end
146-
end
147-
148-
module BuildApi
149-
BUILD_API_BASE = "/api/build/v1"
150-
151-
def self.metadata(file_name : String, commit : String, branch : String, uri : String)
152-
host = URI.parse(Core.build_host)
153-
file_name = URI.encode_www_form(file_name)
154-
ConnectProxy::HTTPClient.new(host) do |client|
155-
path = "#{BUILD_API_BASE}/metadata/#{file_name}"
156-
params = URI::Params.encode({"url" => uri, "branch" => branch, "commit" => commit})
157-
uri = "#{path}?#{params}"
158-
rep = client.get(uri)
159-
Log.debug { {message: "Getting driver metadata. Server respose: #{rep.status_code}", file_name: file_name, commit: commit, branch: branch} }
160-
rep
161-
end
162-
end
163-
164-
def self.defaults(file_name : String, commit : String, branch : String, uri : String)
165-
host = URI.parse(Core.build_host)
166-
file_name = URI.encode_www_form(file_name)
167-
ConnectProxy::HTTPClient.new(host) do |client|
168-
path = "#{BUILD_API_BASE}/defaults/#{file_name}"
169-
params = URI::Params.encode({"url" => uri, "branch" => branch, "commit" => commit})
170-
uri = "#{path}?#{params}"
171-
rep = client.get(uri)
172-
Log.debug { {message: "Getting driver defaults. Server respose: #{rep.status_code}", file_name: file_name, commit: commit, branch: branch} }
173-
rep
174-
end
175-
end
176-
177-
def self.compiled?(file_name : String, commit : String, branch : String, uri : String)
178-
host = URI.parse(Core.build_host)
179-
file_name = URI.encode_www_form(file_name)
180-
ConnectProxy::HTTPClient.new(host) do |client|
181-
path = "#{BUILD_API_BASE}/#{Core::ARCH}/compiled/#{file_name}"
182-
params = URI::Params.encode({"url" => uri, "branch" => branch, "commit" => commit})
183-
uri = "#{path}?#{params}"
184-
rep = client.get(uri)
185-
Log.debug { {message: "Checking if driver is compiled?. Server respose: #{rep.status_code}", file_name: file_name, commit: commit, branch: branch, server_rep: rep.body} }
186-
rep
187-
end
188-
end
189-
190-
def self.compile(file_name : String, url : String, commit : String, branch : String, force : Bool, username : String? = nil, password : String? = nil, fetch : Bool = true)
191-
host = URI.parse(Core.build_host)
192-
file_name = URI.encode_www_form(file_name)
193-
headers = HTTP::Headers.new
194-
headers["X-Git-Username"] = username.not_nil! unless username.nil?
195-
headers["X-Git-Password"] = password.not_nil! unless password.nil?
196-
197-
resp = ConnectProxy::HTTPClient.new(host) do |client|
198-
path = "#{BUILD_API_BASE}/#{Core::ARCH}/#{file_name}"
199-
params = URI::Params.encode({"url" => url, "branch" => branch, "commit" => commit, "force" => force.to_s})
200-
uri = "#{path}?#{params}"
201-
rep = client.post(uri, headers: headers)
202-
Log.debug { {message: "Build URL host : #{client.host}, URI: #{uri} . Server response: #{rep.status_code}", server_resp: rep.body} }
203-
rep
204-
end
205-
206-
raise "Build API returned #{resp.status_code} while 202 was expected. Returned error: #{resp.body}" unless resp.status_code == 202
207-
link = resp.headers["Content-Location"] rescue raise "Build API returned invalid response, missing Content-Location header"
208-
209-
task = JSON.parse(resp.body).as_h
210-
loop do
211-
resp = ConnectProxy::HTTPClient.new(host) do |client|
212-
rep = client.get(link)
213-
Log.debug { {message: "Invoked request: URI: #{link} . Server response: #{rep.status_code}", server_resp: rep.body} }
214-
rep
215-
end
216-
217-
raise "Returned invalid response code: #{resp.status_code}, #{link}, resp: #{resp.body}" unless resp.success? || resp.status_code == 303
218-
task = JSON.parse(resp.body).as_h
219-
break if task["state"].in?("cancelled", "error", "done")
220-
sleep 5.seconds
221-
end
222-
if resp.success? && task["state"].in?("cancelled", "error")
223-
raise task["message"].to_s
224-
end
225-
raise "Build API end-point #{link} returned invalid response code #{resp.status_code}, expected 303" unless resp.status_code == 303
226-
raise "Build API end-point #{link} returned invalid state #{task["state"]}, expected 'done'" unless task["state"] == "done"
227-
hdr = resp.headers["Location"] rescue raise "Build API returned compilation done, but missing Location URL"
228-
if fetch
229-
ConnectProxy::HTTPClient.new(host) do |client|
230-
client.get(hdr)
231-
end
232-
end
233-
end
234-
end
235-
236-
record Result, success : Bool = false, output : String = "", name : String = "", path : String = ""
237-
2387
class DriverResource < Resource(Model::Driver)
2398
private getter? startup : Bool = true
2409
private getter module_manager : ModuleManager
@@ -244,7 +13,7 @@ module PlaceOS::Core
24413
def initialize(
24514
@startup : Bool = true,
24615
@binary_dir : String = "#{Dir.current}/bin/drivers",
247-
@module_manager : ModuleManager = ModuleManager.instance
16+
@module_manager : ModuleManager = ModuleManager.instance,
24817
)
24918
@store = DriverStore.new
25019
buffer_size = System.cpu_count.to_i
@@ -265,6 +34,7 @@ module PlaceOS::Core
26534
driver.update_fields(compilation_output: nil) unless driver.compilation_output.nil?
26635
Resource::Result::Success
26736
in .deleted?
37+
DriverResource.remove_driver(driver, store)
26838
Result::Skipped
26939
end
27040
rescue exception
@@ -275,7 +45,7 @@ module PlaceOS::Core
27545
driver : Model::Driver,
27646
store : DriverStore,
27747
startup : Bool = false,
278-
module_manager : ModuleManager = ModuleManager.instance
48+
module_manager : ModuleManager = ModuleManager.instance,
27949
) : Core::Result
28050
driver_id = driver.id.as(String)
28151
repository = driver.repository!
@@ -361,10 +131,28 @@ module PlaceOS::Core
361131
Log.info { {message: "updated commit on driver", id: driver.id, name: driver.name, commit: commit} }
362132
end
363133

134+
def self.remove_driver(driver : Model::Driver, store : DriverStore)
135+
path = store.driver_binary_path(driver.file_name, driver.commit)
136+
Log.info { {message: "removing driver binary as it got removed from drivers", driver_id: driver.id.as(String), path: path.to_s} }
137+
remove_stale_driver(path, driver.id.as(String))
138+
end
139+
140+
def start_driver_jobs
141+
DriverIntegrity.start_integrity_checker
142+
DriverCleanup.start_cleanup
143+
end
144+
364145
def start
365146
super
366147
@startup = false
148+
start_driver_jobs
367149
self
368150
end
151+
152+
def stop
153+
super
154+
DriverIntegrity.stop_integrity_checker
155+
DriverCleanup.stop_cleanup
156+
end
369157
end
370158
end

0 commit comments

Comments
 (0)