smart_proxy_omaha 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3153cfdac54b440908156dbada4b9af261c53cd5
4
- data.tar.gz: f35c54968e7f16b970c8e34434dfef41afa8bebb
3
+ metadata.gz: 4a6708a296e67836993cc8002728a630e3eb1aa3
4
+ data.tar.gz: 2d1a008d27a6500d01cf0f286d2de64c0a87ed47
5
5
  SHA512:
6
- metadata.gz: 126dc938473bd16fb5c3cdf64d4fef7800610f22ac86473d989b654b75126c2ccb1c8bd0f05b28183de756668f156ba21bb298a3dd083bb52d5b199d9e0ebd9f
7
- data.tar.gz: e40cc9ad0d51dcaf14fdc61a2769daa70bf4da74ce8c676208c55c0b6d6c134a91b8d9723ab83345bf49650649d20c9e825351359d043091b706052a20227e9c
6
+ metadata.gz: 1242a70477fb372e90003aae5d9b1efdc0cb081da403e1ef95f03d58cbe447c0b494ca0eff0829d2ad6ac6a6906ff4e599a236f719cdf9caa7777f4440db382b
7
+ data.tar.gz: ccd486cf864f01b2ac4d76c72ea901de33399a03fd6aeb1a0b8fc6cff8125da562b4aa2c48d44f9c47caeb51ac0c17d615a2f33e030f79cd51d3d2eb20930022
data/README.md CHANGED
@@ -28,17 +28,25 @@ Do not forget to register the smart proxy in Foreman via the user interface.
28
28
  ## Host Configuration
29
29
 
30
30
  You need to configure your CoreOS hosts to connect to the Omaha smart-proxy for updates. You can either configure your servers manually or use cloud-config.
31
+ If your smart-proxy uses a self-signed ssl certificate, you have to add the CA certificate to the CoreOS truststore. By default, the smart-proxys uses a PuppetCA certificate. To print the PuppetCA certificate, issue `cat $(puppet config print localcacert)` on any puppet enabled node.
31
32
 
32
33
  ### Using Config File
33
34
 
34
- Edit `/etc/coreos/update.conf`
35
+ To add a custom CA certificate to CoreOS's truststore:
36
+
37
+ ```bash
38
+ vim /etc/ssl/certs/customCA_root.pem
39
+ sudo /usr/sbin/update-ca-certificates
40
+ ```
41
+
42
+ To configure CoreOS to connect to the Omaha smart-proxy for updates, edit `/etc/coreos/update.conf`:
35
43
 
36
44
  ```
37
45
  GROUP=stable
38
46
  SERVER=https://omahaproxy.example.com:8443/omaha/v1/update
39
47
  ```
40
48
 
41
- Restart update engine
49
+ Restart update engine:
42
50
 
43
51
  ```bash
44
52
  sudo systemctl restart update-engine
@@ -46,6 +54,8 @@ sudo systemctl restart update-engine
46
54
 
47
55
  ### Using Cloud-Config
48
56
 
57
+ Configure CoreOS to connect to the Omaha smart-proxy for updates:
58
+
49
59
  ```yaml
50
60
  #cloud-config
51
61
  coreos:
@@ -54,6 +64,35 @@ coreos:
54
64
  server: "https://omahaproxy.example.com:8443/omaha/v1/update"
55
65
  ```
56
66
 
67
+ Add a custom CA certificate to CoreOS's truststore:
68
+ ```yaml
69
+ #cloud-config
70
+ write-files:
71
+ - path: /etc/ssl/certs/customCA_root.pem
72
+ permissions: 0644
73
+ content: |
74
+ -----BEGIN CERTIFICATE-----
75
+ YOUR-BASE64-ENCODED-CERTIFICATE
76
+ -----END CERTIFICATE-----
77
+ units:
78
+ - name: update-ca-certificates.service
79
+ command: start
80
+ content: |
81
+ [Unit]
82
+ Description=Force Update CA bundle at /etc/ssl/certs/ca-certificates.crt
83
+ # Since other services depend on the certificate store run this early
84
+ DefaultDependencies=no
85
+ Wants=systemd-tmpfiles-setup.service clean-ca-certificates.service
86
+ After=systemd-tmpfiles-setup.service clean-ca-certificates.service
87
+ Before=sysinit.target
88
+ ConditionPathIsReadWrite=/etc/ssl/certs
89
+
90
+ [Service]
91
+ Type=oneshot
92
+ ExecStart=/usr/sbin/update-ca-certificates
93
+ ```
94
+
95
+
57
96
  ### Release channels
58
97
 
59
98
  All three default release channels (alpha, beta, stable) are supported. You cannot define custom channels right now.
@@ -15,6 +15,9 @@ module Proxy::LogBuffer
15
15
  end
16
16
  include Proxy::Log
17
17
 
18
+ # Unload all other plugins
19
+ ::Proxy::Plugins.instance.loaded.delete_if { |plugin| plugin[:name] != :omaha }
20
+
18
21
  ::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins
19
22
 
20
23
  unless ::Proxy::Plugins.instance.plugin_enabled?(:omaha)
@@ -1,16 +1,19 @@
1
1
  require 'thread'
2
+ require 'base64'
2
3
  require 'smart_proxy_omaha/http_shared'
4
+ require 'smart_proxy_omaha/http_verify'
3
5
 
4
6
  module Proxy::Omaha
5
7
  class HttpDownload
6
8
  include Proxy::Log
7
9
  include HttpShared
8
10
 
9
- attr_accessor :dst, :src, :result
11
+ attr_accessor :dst, :src, :tmp, :result, :http_response
10
12
 
11
13
  def initialize(src, dst)
12
14
  @src = src
13
15
  @dst = dst
16
+ @tmp = Tempfile.new('download', File.dirname(dst))
14
17
  end
15
18
 
16
19
  def start
@@ -22,44 +25,71 @@ module Proxy::Omaha
22
25
  end
23
26
 
24
27
  def run
25
- with_filelock do
26
- logger.info "Downloading #{src} to #{dst}."
27
- res = download
28
- logger.info "Finished downloading #{dst}."
29
- res
28
+ logger.info "#{filename}: Downloading #{src} to #{dst}."
29
+ unless download
30
+ logger.error "#{filename} failed to download."
31
+ return false
30
32
  end
33
+ logger.info "#{filename}: Finished downloading #{dst}."
34
+ unless valid?
35
+ logger.error "#{filename} is not valid. Deleting corrupt file."
36
+ File.unlink(tmp)
37
+ return false
38
+ end
39
+ # no DIGESTS file is provided for update.gz
40
+ # so we need to generate our own based on the
41
+ # http headers
42
+ write_digest if filename == 'update.gz'
43
+ finish
44
+ ensure
45
+ tmp.unlink
46
+ true
31
47
  end
32
48
 
33
49
  def join
34
50
  @task.join
35
51
  end
36
52
 
53
+ def valid?
54
+ verifier.valid?
55
+ end
56
+
57
+ def finish
58
+ File.rename(tmp, dst)
59
+ true
60
+ end
61
+
62
+ def write_digest
63
+ hexdigest = Digest.hexencode(Base64.decode64(verifier.local_md5))
64
+ File.open("#{dst}.DIGESTS", 'w') { |file| file.write("#{hexdigest} #{filename}\n") }
65
+ end
66
+
37
67
  private
38
68
 
69
+ def verifier
70
+ @verifier ||= HttpVerify.new(
71
+ :local_file => tmp,
72
+ :http_request => http_response,
73
+ :filename => filename,
74
+ )
75
+ end
76
+
77
+ def filename
78
+ File.basename(dst)
79
+ end
80
+
39
81
  def download
40
82
  http, request = connection_factory(src)
41
83
 
42
- http.request(request) do |response|
43
- open(dst, 'w') do |io|
84
+ self.http_response = http.request(request) do |response|
85
+ open(tmp, 'w') do |io|
44
86
  response.read_body do |chunk|
45
87
  io.write chunk
46
88
  end
47
89
  end
48
90
  end
49
- true
50
- end
51
91
 
52
- def with_filelock
53
- lock = Proxy::FileLock.try_locking(dst)
54
- if lock.nil?
55
- false
56
- else
57
- begin
58
- yield
59
- ensure
60
- Proxy::FileLock.unlock(lock)
61
- end
62
- end
92
+ true
63
93
  end
64
94
  end
65
95
  end
@@ -16,5 +16,17 @@ module Proxy::Omaha
16
16
  response.body
17
17
  end
18
18
  end
19
+
20
+ def head(url)
21
+ http, request = connection_factory(url, :method => :head)
22
+
23
+ Timeout::timeout(10) do
24
+ response = http.request(request)
25
+
26
+ raise "Error retrieving from #{url}: #{response.class}" unless ["200", "201"].include?(response.code)
27
+
28
+ response
29
+ end
30
+ end
19
31
  end
20
32
  end
@@ -4,7 +4,8 @@ require 'uri'
4
4
 
5
5
  module Proxy::Omaha
6
6
  module HttpShared
7
- def connection_factory(url)
7
+ def connection_factory(url, opts = {})
8
+ method = opts.fetch(:method, :get)
8
9
  uri = URI.parse(url)
9
10
 
10
11
  if Proxy::Omaha::Plugin.settings.proxy.to_s.empty?
@@ -22,7 +23,15 @@ module Proxy::Omaha
22
23
  http.use_ssl = true
23
24
  end
24
25
 
25
- request = Net::HTTP::Get.new(uri.request_uri)
26
+ request_class = case method
27
+ when :get
28
+ Net::HTTP::Get
29
+ when :head
30
+ Net::HTTP::Head
31
+ else
32
+ raise "Unknown request class"
33
+ end
34
+ request = request_class.new(uri.request_uri)
26
35
 
27
36
  [http, request]
28
37
  end
@@ -0,0 +1,73 @@
1
+ module Proxy::Omaha
2
+ class HttpVerify
3
+ include Proxy::Log
4
+
5
+ attr_accessor :http_request, :local_file, :filename
6
+
7
+ def initialize(opts = {})
8
+ self.http_request = opts.fetch(:http_request)
9
+ self.local_file = opts.fetch(:local_file)
10
+ self.filename = opts.fetch(:filename, File.basename(local_file))
11
+ end
12
+
13
+ def valid?
14
+ logger.debug "#{filename}: Verifying if file is valid."
15
+ return false unless file_size_valid?
16
+ return false if headers['x-goog-hash'] && !md5_hash_valid?
17
+ true
18
+ end
19
+
20
+ def file_size_valid?
21
+ unless remote_size
22
+ logger.error "#{filename}: Cannot determine remote file size."
23
+ return false
24
+ end
25
+ unless local_size == remote_size
26
+ logger.debug "#{filename}: File sizes do not match. Remote: #{remote_size}, Local: #{local_size}"
27
+ return false
28
+ end
29
+ true
30
+ end
31
+
32
+ def local_size
33
+ File.size(local_file)
34
+ end
35
+
36
+ def remote_size
37
+ @remote_size ||= content_length
38
+ end
39
+
40
+ def md5_hash_valid?
41
+ unless local_md5 == remote_md5
42
+ logger.debug "#{filename}: MD5 checksums do not match. Remote: #{remote_md5}, Local: #{local_md5}"
43
+ return false
44
+ end
45
+ true
46
+ end
47
+
48
+ def remote_hashes
49
+ headers['x-goog-hash'].inject({}) do |hsh, header|
50
+ key, value = header.split('=', 2)
51
+ hsh[key] = value
52
+ hsh
53
+ end
54
+ end
55
+
56
+ def local_md5
57
+ @local_md5 ||= Digest::MD5.file(local_file).base64digest
58
+ end
59
+
60
+ def remote_md5
61
+ remote_hashes['md5']
62
+ end
63
+
64
+ def content_length
65
+ length = headers['content-length'] || headers['x-goog-stored-content-length']
66
+ length.first.to_i if length
67
+ end
68
+
69
+ def headers
70
+ @headers ||= http_request.to_hash
71
+ end
72
+ end
73
+ end
@@ -1,5 +1,6 @@
1
1
  require 'sinatra'
2
2
  require 'smart_proxy_omaha/omaha_protocol'
3
+ require 'smart_proxy_omaha/release_repository'
3
4
 
4
5
  module Proxy::Omaha
5
6
 
@@ -27,7 +28,27 @@ module Proxy::Omaha
27
28
  :metadata_provider => metadata_provider
28
29
  )
29
30
  response = omaha_handler.handle
31
+ status response.http_status
30
32
  response.to_xml
31
33
  end
34
+
35
+ get '/tracks' do
36
+ release_repository.tracks.map do |track|
37
+ {
38
+ :name => track,
39
+ :architectures => release_repository.architectures(track)
40
+ }
41
+ end.to_json
42
+ end
43
+
44
+ get '/tracks/:track/:architecture' do |track, architecture|
45
+ not_found unless release_repository.tracks.include?(track)
46
+ not_found unless release_repository.architectures(track).include?(architecture)
47
+ release_repository.releases(track, architecture).map do |release|
48
+ release.to_h.merge(
49
+ :file_urls => release.file_urls(request.base_url)
50
+ )
51
+ end.to_json
52
+ end
32
53
  end
33
54
  end
@@ -0,0 +1,12 @@
1
+ module Proxy::Omaha::OmahaProtocol
2
+ class Errorinternalresponse < Response
3
+
4
+ def http_status
5
+ 500
6
+ end
7
+
8
+ protected
9
+
10
+ def xml_response(xml); end
11
+ end
12
+ end
@@ -1,3 +1,5 @@
1
+ require 'smart_proxy_omaha/track'
2
+
1
3
  module Proxy::Omaha::OmahaProtocol
2
4
  class Handler
3
5
  include ::Proxy::Log
@@ -22,7 +24,7 @@ module Proxy::Omaha::OmahaProtocol
22
24
  )
23
25
  end
24
26
 
25
- unless ['stable', 'beta', 'alpha'].include?(request.track)
27
+ unless Proxy::Omaha::Track.valid?(request.track)
26
28
  logger.error "Unknown track requested. Aborting Omaha request."
27
29
  return Proxy::Omaha::OmahaProtocol::Eventacknowledgeresponse.new(
28
30
  :appid => request.appid,
@@ -32,13 +34,21 @@ module Proxy::Omaha::OmahaProtocol
32
34
  end
33
35
 
34
36
  upload_facts
35
- process_report
37
+ process_report if request.event?
36
38
 
37
- if request.updatecheck
39
+ if request.updatecheck?
38
40
  handle_update
39
- else
41
+ elsif request.event?
40
42
  handle_event
43
+ elsif request.ping?
44
+ handle_ping
45
+ else
46
+ logger.info "OmahaHandler: Unknown request."
47
+ handle_error
41
48
  end
49
+ rescue StandardError => e
50
+ logger.error("OmahaHandler: Aw, Snap! Error: #{e}", e.backtrace)
51
+ handle_error
42
52
  end
43
53
 
44
54
  private
@@ -48,12 +58,29 @@ module Proxy::Omaha::OmahaProtocol
48
58
  end
49
59
 
50
60
  def handle_event
61
+ logger.info "OmahaHandler: Processing event."
51
62
  Proxy::Omaha::OmahaProtocol::Eventacknowledgeresponse.new(
52
63
  :appid => request.appid,
53
64
  :base_url => request.base_url
54
65
  )
55
66
  end
56
67
 
68
+ def handle_ping
69
+ logger.info "OmahaHandler: Processing ping."
70
+ Proxy::Omaha::OmahaProtocol::Pingresponse.new(
71
+ :appid => request.appid,
72
+ :base_url => request.base_url
73
+ )
74
+ end
75
+
76
+ def handle_error
77
+ Proxy::Omaha::OmahaProtocol::Errorinternalresponse.new(
78
+ :appid => request.appid,
79
+ :base_url => request.base_url,
80
+ :status => 'error-internal',
81
+ )
82
+ end
83
+
57
84
  def process_report
58
85
  report = {
59
86
  'host' => request.hostname,
@@ -66,7 +93,8 @@ module Proxy::Omaha::OmahaProtocol
66
93
 
67
94
  def handle_update
68
95
  latest_os = repository.latest_os(request.track, request.board)
69
- if !latest_os.nil? && latest_os > Gem::Version.new(request.version)
96
+ if !latest_os.nil? && latest_os.version > Gem::Version.new(request.version)
97
+ logger.info "OmahaHandler: Offering update from #{request.version} to #{latest_os.version}"
70
98
  Proxy::Omaha::OmahaProtocol::Updateresponse.new(
71
99
  :appid => request.appid,
72
100
  :metadata => metadata_provider.get(request.track, latest_os, request.board),
@@ -74,6 +102,7 @@ module Proxy::Omaha::OmahaProtocol
74
102
  :base_url => request.base_url
75
103
  )
76
104
  else
105
+ logger.info "OmahaHandler: No update."
77
106
  Proxy::Omaha::OmahaProtocol::Noupdateresponse.new(
78
107
  :appid => request.appid,
79
108
  :base_url => request.base_url
@@ -0,0 +1,9 @@
1
+ module Proxy::Omaha::OmahaProtocol
2
+ class Pingresponse < Response
3
+ protected
4
+
5
+ def xml_response(xml)
6
+ xml.ping(:status => 'ok')
7
+ end
8
+ end
9
+ end
@@ -6,7 +6,7 @@ module Proxy::Omaha::OmahaProtocol
6
6
  attr_reader :appid, :version, :track, :updatecheck, :eventtype, :eventresult, :board,
7
7
  :alephversion, :oemversion, :oem,
8
8
  :platform, :osmajor, :osminor, :hostname, :ipaddress, :ipaddress6,
9
- :body, :ip, :base_url
9
+ :body, :ip, :base_url, :ping
10
10
 
11
11
  def initialize(body, options)
12
12
  @body = body
@@ -46,6 +46,18 @@ module Proxy::Omaha::OmahaProtocol
46
46
  appid == Proxy::Omaha::OmahaProtocol::COREOS_APPID
47
47
  end
48
48
 
49
+ def updatecheck?
50
+ !@updatecheck.empty?
51
+ end
52
+
53
+ def ping?
54
+ !@ping.empty?
55
+ end
56
+
57
+ def event?
58
+ !@event.empty?
59
+ end
60
+
49
61
  private
50
62
 
51
63
  def parse_request
@@ -61,7 +73,9 @@ module Proxy::Omaha::OmahaProtocol
61
73
  @oem = xml_request.xpath('/request/app/@oem').to_s
62
74
  @platform = xml_request.xpath('/request/os/@platform').to_s
63
75
  @platform = 'CoreOS' if @platform.empty?
64
- @updatecheck = !xml_request.xpath('/request/app/updatecheck').to_s.empty?
76
+ @updatecheck = xml_request.xpath('/request/app/updatecheck').to_s
77
+ @ping = xml_request.xpath('/request/app/ping').to_s
78
+ @event = xml_request.xpath('/request/app/event').to_s
65
79
  @eventtype = xml_request.xpath('/request/app/event/@eventtype').to_s.to_i
66
80
  @eventresult = xml_request.xpath('/request/app/event/@eventresult').to_s.to_i
67
81
  end
@@ -17,6 +17,10 @@ module Proxy::Omaha::OmahaProtocol
17
17
  xml.to_xml
18
18
  end
19
19
 
20
+ def http_status
21
+ 200
22
+ end
23
+
20
24
  protected
21
25
 
22
26
  def xml
@@ -7,6 +7,8 @@ require 'smart_proxy_omaha/omaha_protocol/request'
7
7
  require 'smart_proxy_omaha/omaha_protocol/eventacknowledgeresponse'
8
8
  require 'smart_proxy_omaha/omaha_protocol/noupdateresponse'
9
9
  require 'smart_proxy_omaha/omaha_protocol/updateresponse'
10
+ require 'smart_proxy_omaha/omaha_protocol/pingresponse'
11
+ require 'smart_proxy_omaha/omaha_protocol/errorinternalresponse'
10
12
  require 'smart_proxy_omaha/omaha_protocol/handler'
11
13
 
12
14
  module Proxy::Omaha::OmahaProtocol
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'digest/md5'
2
3
  require 'smart_proxy_omaha/http_download'
3
4
  require 'smart_proxy_omaha/metadata_provider'
4
5
 
@@ -7,6 +8,7 @@ module Proxy::Omaha
7
8
  include Proxy::Log
8
9
 
9
10
  attr_accessor :track, :version, :architecture
11
+ attr_writer :digests
10
12
 
11
13
  def initialize(options)
12
14
  @track = options.fetch(:track).to_s
@@ -14,8 +16,16 @@ module Proxy::Omaha
14
16
  @version = Gem::Version.new(options.fetch(:version))
15
17
  end
16
18
 
19
+ def base_path
20
+ @base_path ||= File.join(Proxy::Omaha::Plugin.settings.contentpath, track, architecture)
21
+ end
22
+
17
23
  def path
18
- @path ||= File.join(Proxy::Omaha::Plugin.settings.contentpath, track, architecture, version.to_s)
24
+ @path ||= File.join(base_path, version.to_s)
25
+ end
26
+
27
+ def current_path
28
+ @current_path ||= File.join(base_path, 'current')
19
29
  end
20
30
 
21
31
  def metadata
@@ -27,7 +37,10 @@ module Proxy::Omaha
27
37
  end
28
38
 
29
39
  def valid?
30
- expected_files_exist?
40
+ existing_files.select { |file| file !~ /\.(DIGESTS|sig)$/ }.map do |file|
41
+ next unless digests[file]
42
+ digests[file].include?(Digest::MD5.file(File.join(path, file)).hexdigest)
43
+ end.compact.all?
31
44
  end
32
45
 
33
46
  def create
@@ -38,6 +51,17 @@ module Proxy::Omaha
38
51
  true
39
52
  end
40
53
 
54
+ def current?
55
+ return false unless File.symlink?(current_path)
56
+ File.readlink(current_path) == path
57
+ end
58
+
59
+ def mark_as_current!
60
+ return true if current?
61
+ File.unlink(current_path) if File.symlink?(current_path)
62
+ FileUtils.ln_s(path, current_path)
63
+ end
64
+
41
65
  def <=>(other)
42
66
  return unless self.class === other
43
67
  version.<=>(other.version)
@@ -54,12 +78,13 @@ module Proxy::Omaha
54
78
  def download
55
79
  sources.map do |url|
56
80
  file = URI.parse(url).path.split('/').last
81
+ next if file_exists?(file)
57
82
  dst = File.join(path, file)
58
83
  logger.debug "Downloading file #{url} to #{dst}"
59
84
  task = ::Proxy::Omaha::HttpDownload.new(url, dst)
60
85
  task.start
61
86
  task
62
- end.each(&:join).map(&:result).all?
87
+ end.compact.each(&:join).map(&:result).all?
63
88
  end
64
89
 
65
90
  def create_metadata
@@ -91,9 +116,17 @@ module Proxy::Omaha
91
116
  upstream = "https://#{track}.release.core-os.net/#{architecture}/#{version}"
92
117
  [
93
118
  "#{upstream}/coreos_production_pxe.vmlinuz",
119
+ "#{upstream}/coreos_production_pxe.DIGESTS",
94
120
  "#{upstream}/coreos_production_image.bin.bz2",
95
121
  "#{upstream}/coreos_production_image.bin.bz2.sig",
122
+ "#{upstream}/coreos_production_image.bin.bz2.DIGESTS",
96
123
  "#{upstream}/coreos_production_pxe_image.cpio.gz",
124
+ "#{upstream}/coreos_production_pxe_image.cpio.gz.DIGESTS",
125
+ "#{upstream}/coreos_production_vmware_raw_image.bin.bz2",
126
+ "#{upstream}/coreos_production_vmware_raw_image.bin.bz2.sig",
127
+ "#{upstream}/coreos_production_vmware_raw_image.bin.bz2.DIGESTS",
128
+ "#{upstream}/version.txt",
129
+ "#{upstream}/version.txt.DIGESTS",
97
130
  "https://update.release.core-os.net/#{architecture}/#{version}/update.gz"
98
131
  ]
99
132
  end
@@ -102,8 +135,24 @@ module Proxy::Omaha
102
135
  sources.map { |source| File.basename(source) }
103
136
  end
104
137
 
105
- def expected_files_exist?
106
- expected_files.map {|file| File.file?(File.join(path, file)) }.all?
138
+ def existing_files
139
+ Dir.glob(File.join(path, '*')).map { |file| File.basename(file) }
140
+ end
141
+
142
+ def missing_files
143
+ expected_files - existing_files
144
+ end
145
+
146
+ def digest_files
147
+ Dir.glob(File.join(path, '*.DIGESTS')).map { |file| File.basename(file) }
148
+ end
149
+
150
+ def complete?
151
+ missing_files.empty?
152
+ end
153
+
154
+ def file_exists?(file)
155
+ File.file?(File.join(path, file))
107
156
  end
108
157
 
109
158
  def purge
@@ -114,6 +163,45 @@ module Proxy::Omaha
114
163
  false
115
164
  end
116
165
 
166
+ def digests
167
+ @digests ||= load_digests!
168
+ end
169
+
170
+ def load_digests!
171
+ self.digests = {}
172
+ digest_files.each do |digest_file|
173
+ file = File.basename(digest_file, '.DIGESTS')
174
+ File.readlines(File.join(path, digest_file)).each do |line|
175
+ next unless line =~ /^\w+[ ]+\S+$/
176
+ hexdigest = line.split(/[ ]+/).first
177
+ self.digests[file] ||= []
178
+ self.digests[file] << hexdigest
179
+ end
180
+ end
181
+ self.digests
182
+ end
183
+
184
+ def to_h
185
+ {
186
+ :name => to_s,
187
+ :current => current?,
188
+ :architecture => architecture,
189
+ :track => track,
190
+ :complete => complete?,
191
+ :files => existing_files
192
+ }
193
+ end
194
+
195
+ def to_json
196
+ to_h.to_json
197
+ end
198
+
199
+ def file_urls(base_url)
200
+ existing_files.map do |file|
201
+ [base_url, 'omahareleases', track, architecture, self, file].join('/')
202
+ end
203
+ end
204
+
117
205
  private
118
206
 
119
207
  def metadata_provider
@@ -1,7 +1,25 @@
1
+ require 'smart_proxy_omaha/release'
2
+
1
3
  module Proxy::Omaha
2
4
  class ReleaseRepository
3
5
  def releases(track, architecture)
4
- Dir.glob(File.join(Proxy::Omaha::Plugin.settings.contentpath, track, architecture, '*')).select {|f| File.directory? f }.map { |f| Gem::Version.new(File.basename(f)) }
6
+ Dir.glob(File.join(Proxy::Omaha::Plugin.settings.contentpath, track, architecture, '*')).select do |f|
7
+ File.directory?(f) && ! File.symlink?(f)
8
+ end.map do |f|
9
+ Proxy::Omaha::Release.new(
10
+ :track => track,
11
+ :architecture => architecture,
12
+ :version => File.basename(f)
13
+ )
14
+ end
15
+ end
16
+
17
+ def tracks
18
+ Dir.glob(File.join(Proxy::Omaha::Plugin.settings.contentpath, '*')).select {|f| File.directory? f }.map { |f| File.basename(f) }
19
+ end
20
+
21
+ def architectures(track)
22
+ Dir.glob(File.join(Proxy::Omaha::Plugin.settings.contentpath, track, '*')).select {|f| File.directory? f }.map { |f| File.basename(f) }
5
23
  end
6
24
 
7
25
  def latest_os(track, architecture)
@@ -1,4 +1,5 @@
1
1
  require 'smart_proxy_omaha/release'
2
+ require 'smart_proxy_omaha/track'
2
3
  require 'smart_proxy_omaha/release_provider'
3
4
 
4
5
  module Proxy::Omaha
@@ -11,25 +12,32 @@ module Proxy::Omaha
11
12
  return
12
13
  end
13
14
 
14
- ['alpha', 'beta', 'stable'].each do |track|
15
+ Proxy::Omaha::Track.all.each do |track|
15
16
  logger.debug "Syncing track: #{track}..."
16
- sync_track(track)
17
+ releases = release_provider(track).releases
18
+ releases.last(sync_count).each do |release|
19
+ sync_release(track, release)
20
+ end
21
+ update_current_release(track, releases.last) if releases.any?
17
22
  end
18
23
  end
19
24
 
20
- def sync_track(track)
21
- release_provider(track).releases.last(sync_count).each do |release|
22
- if release.exists?
23
- if release.valid?
24
- logger.info "#{track} release #{release} already exists and is valid."
25
- next
26
- else
27
- logger.info "#{track} release #{release} is invalid. Purging."
28
- release.purge
29
- end
25
+ def sync_release(track, release)
26
+ if release.exists?
27
+ if !release.valid?
28
+ logger.info "#{track} release #{release} is invalid. Purging."
29
+ release.purge
30
+ elsif release.complete?
31
+ logger.info "#{track} release #{release} exists, is complete and valid. Skipping sync."
32
+ return
30
33
  end
31
- release.create
32
34
  end
35
+ release.create
36
+ end
37
+
38
+ def update_current_release(track, release)
39
+ logger.debug "#{track}: Updating current release to #{release}"
40
+ release.mark_as_current!
33
41
  end
34
42
 
35
43
  private
@@ -0,0 +1,15 @@
1
+ module Proxy
2
+ module Omaha
3
+ module Track
4
+ TRACKS = ['alpha', 'beta', 'stable'].freeze
5
+
6
+ def self.all
7
+ TRACKS
8
+ end
9
+
10
+ def self.valid?(track)
11
+ all.include?(track)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
2
  module Omaha
3
- VERSION = '0.0.2'
3
+ VERSION = '0.0.3'
4
4
  end
5
5
  end
@@ -0,0 +1,6 @@
1
+ <request protocol="3.0" version="update_engine-0.4.2" updaterversion="update_engine-0.4.2" installsource="scheduler" ismachine="1">
2
+ <os version="Chateau" platform="CoreOS" sp="1409.7.0_x86_64"></os>
3
+ <app appid="{e96281a6-d1af-4bde-9a0a-97b76e56dc57}" version="1409.7.0" track="stable" bootid="{9f849398-6c5d-4ad2-af52-07fd168ff548}" oem="vmware" oemversion="10.1.5" alephversion="1409.7.0" machineid="904694c1f8e148f0a77ef0884711da1a" lang="en-US" board="amd64-usr" hardware_class="" delta_okay="false">
4
+ <ping active="1"></ping>
5
+ </app>
6
+ </request>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <response protocol="3.0" server="example.org">
3
+ <daystart elapsed_seconds="0"/>
4
+ <app app_id="e96281a6-d1af-4bde-9a0a-97b76e56dc57" status="error-internal" />
5
+ </response>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <response protocol="3.0" server="example.org">
3
+ <daystart elapsed_seconds="0"/>
4
+ <app app_id="e96281a6-d1af-4bde-9a0a-97b76e56dc57" status="ok">
5
+ <ping status="ok"/>
6
+ </app>
7
+ </response>
@@ -16,7 +16,14 @@ class HttpDownloadTest < Test::Unit::TestCase
16
16
  end
17
17
 
18
18
  def test_downloads_file
19
- stub_request(:get, @source_url).to_return(:status => [200, 'OK'], :body => "body")
19
+ stub_request(:get, @source_url).to_return(
20
+ :status => [200, 'OK'],
21
+ :body => 'body',
22
+ headers: {
23
+ 'Content-Length' => 4,
24
+ 'x-goog-hash' => 'md5=hBotaJrYa9FhFEdFPCLG/A=='
25
+ }
26
+ )
20
27
 
21
28
  http_download = ::Proxy::Omaha::HttpDownload.new(@source_url, @destination_path)
22
29
  task = http_download.start
@@ -24,11 +31,4 @@ class HttpDownloadTest < Test::Unit::TestCase
24
31
  assert_equal 'body', File.read(@destination_path)
25
32
  assert_equal true, http_download.result
26
33
  end
27
-
28
- def test_should_skip_download_if_one_is_in_progress
29
- locked = Proxy::FileLock.try_locking(@destination_path)
30
- http_download = ::Proxy::Omaha::HttpDownload.new(@source_url, locked.path)
31
- http_download.start.join
32
- assert_equal false, http_download.result
33
- end
34
34
  end
@@ -11,7 +11,21 @@ end
11
11
 
12
12
  class TestReleaseRepository
13
13
  def releases(track, architecture)
14
- ['1068.9.0', '1122.2.0'].map { |release| Gem::Version.new(release) }
14
+ ['1068.9.0', '1122.2.0'].map do |release|
15
+ Proxy::Omaha::Release.new(
16
+ :track => 'alpha',
17
+ :architecture => 'amd64-usr',
18
+ :version => release
19
+ )
20
+ end
21
+ end
22
+
23
+ def tracks
24
+ ['alpha', 'beta', 'stable']
25
+ end
26
+
27
+ def architectures(track)
28
+ ['amd64-usr']
15
29
  end
16
30
 
17
31
  def latest_os(track, architecture)
@@ -73,4 +87,33 @@ class OmahaApiTest < Test::Unit::TestCase
73
87
  assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
74
88
  assert_xml_equal xml_fixture('response_update_complete_error'), last_response.body
75
89
  end
90
+
91
+ def test_processes_ping
92
+ post "/v1/update", xml_fixture('request_ping')
93
+ assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
94
+ assert_xml_equal xml_fixture('response_ping'), last_response.body
95
+ end
96
+
97
+ def test_processes_internalerror
98
+ TestForemanClient.any_instance.stubs(:post_facts).raises(StandardError.new('Test Error'))
99
+ post "/v1/update", xml_fixture('request_update_complete_update')
100
+ refute last_response.ok?
101
+ assert_xml_equal xml_fixture('response_errorinternal'), last_response.body
102
+ end
103
+
104
+ def test_get_tracks
105
+ get "/tracks"
106
+ assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
107
+ parsed = JSON.parse(last_response.body)
108
+ assert_kind_of Array, parsed
109
+ assert_equal ['alpha', 'beta', 'stable'], parsed.map { |track| track['name'] }
110
+ end
111
+
112
+ def test_get_releases
113
+ get "/tracks/alpha/amd64-usr"
114
+ assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
115
+ parsed = JSON.parse(last_response.body)
116
+ assert_kind_of Array, parsed
117
+ assert_equal ['1068.9.0', '1122.2.0'], parsed.map { |track| track['name'] }
118
+ end
76
119
  end
@@ -25,6 +25,10 @@ class RequestTest < Test::Unit::TestCase
25
25
  assert_equal 3, @request.eventtype
26
26
  assert_equal 2, @request.eventresult
27
27
  assert @request.updatecheck
28
+ assert @request.ping
29
+ assert_equal true, @request.ping?
30
+ assert_equal true, @request.event?
31
+ assert_equal true, @request.updatecheck?
28
32
  end
29
33
 
30
34
  def test_from_coreos
@@ -0,0 +1,40 @@
1
+ require 'test_helper'
2
+ require 'tmpdir'
3
+ require 'smart_proxy_omaha/release_repository'
4
+
5
+ class ReleaseRepositoryTest < Test::Unit::TestCase
6
+ def setup
7
+ @contentpath = Dir.mktmpdir
8
+ Proxy::Omaha::Plugin.load_test_settings(
9
+ {
10
+ :contentpath => @contentpath
11
+ }
12
+ )
13
+ @repository = Proxy::Omaha::ReleaseRepository.new
14
+ end
15
+
16
+ def teardown
17
+ FileUtils.rm_rf(@contentpath)
18
+ end
19
+
20
+ def test_releases
21
+ test_releases = [
22
+ '197.0.0', '206.0.0', '206.1.0', '225.1.0', '225.2.0', '226.0.0'
23
+ ]
24
+ test_releases.each do |test_release|
25
+ FileUtils.mkdir_p(release_path(test_release))
26
+ end
27
+
28
+ FileUtils.ln_s(release_path('current'), release_path('226.0.0'))
29
+
30
+ releases = @repository.releases('alpha', 'amd64-usr')
31
+ assert_equal test_releases.sort, releases.map(&:to_s).sort
32
+ assert_equal '226.0.0', @repository.latest_os('alpha', 'amd64-usr').to_s
33
+ end
34
+
35
+ private
36
+
37
+ def release_path(version)
38
+ File.join(@contentpath, 'alpha', 'amd64-usr', version)
39
+ end
40
+ end
@@ -23,8 +23,10 @@ class ReleaseTest < Test::Unit::TestCase
23
23
  FileUtils.rm_rf(@contentpath)
24
24
  end
25
25
 
26
- def test_path
26
+ def test_paths
27
+ assert_equal "#{@contentpath}/stable/amd64-usr", @release.base_path
27
28
  assert_equal "#{@contentpath}/stable/amd64-usr/1068.9.0", @release.path
29
+ assert_equal "#{@contentpath}/stable/amd64-usr/current", @release.current_path
28
30
  end
29
31
 
30
32
  def test_exists?
@@ -84,21 +86,22 @@ class ReleaseTest < Test::Unit::TestCase
84
86
  end
85
87
 
86
88
  def test_download
87
- stub_request(:get, /.*release\.core-os.*/).to_return(:status => [200, 'OK'], :body => "body")
88
- expected = [
89
- 'coreos_production_pxe.vmlinuz',
90
- 'coreos_production_pxe_image.cpio.gz',
91
- 'coreos_production_image.bin.bz2',
92
- 'coreos_production_image.bin.bz2.sig',
93
- 'update.gz'
94
- ]
89
+ stub_request(:get, /.*release\.core-os.*/).to_return(
90
+ :status => [200, 'OK'],
91
+ :body => 'body',
92
+ headers: {
93
+ 'Content-Length' => 4,
94
+ 'x-goog-hash' => 'md5=hBotaJrYa9FhFEdFPCLG/A=='
95
+ }
96
+ )
95
97
 
96
98
  assert_equal true, @release.create_path
97
99
  assert_equal true, @release.download
98
100
 
99
101
  existing_files = Dir.entries(@release.path) - ['.', '..']
100
102
 
101
- assert_equal expected.sort, existing_files.sort
103
+ assert_equal (expected_release_files + ['update.gz.DIGESTS']).sort, existing_files.sort
104
+ assert_equal "841a2d689ad86bd1611447453c22c6fc update.gz\n", File.read(File.join(@release.path, 'update.gz.DIGESTS'))
102
105
  end
103
106
 
104
107
  def test_create_metadata
@@ -117,4 +120,86 @@ class ReleaseTest < Test::Unit::TestCase
117
120
  assert File.exist?(file)
118
121
  assert_equal JSON.parse(expected), JSON.parse(File.read(file))
119
122
  end
123
+
124
+ def test_missing_existing_files
125
+ FileUtils.mkdir_p(@release.path)
126
+ refute @release.complete?
127
+ assert_empty @release.existing_files
128
+ assert_equal expected_release_files.sort, @release.missing_files.sort
129
+
130
+ expected_release_files.each do |file|
131
+ FileUtils.touch(File.join(@release.path, file))
132
+ end
133
+
134
+ assert @release.complete?
135
+ assert_empty @release.missing_files
136
+ assert_equal expected_release_files.sort, @release.existing_files.sort
137
+ end
138
+
139
+ def test_current_release_idempotent
140
+ FileUtils.mkdir_p(@release.path)
141
+ refute @release.current?
142
+ @release.mark_as_current!
143
+ assert @release.current?
144
+ symlinks = Dir.glob("#{@contentpath}/**/*").select { |f| File.symlink?(f) }
145
+ assert_equal 1, symlinks.count
146
+ @release.mark_as_current!
147
+ assert @release.current?
148
+ assert_equal symlinks, Dir.glob("#{@contentpath}/**/*").select { |f| File.symlink?(f) }
149
+ assert_equal @release.path, File.readlink(@release.current_path)
150
+ end
151
+
152
+ def test_current_release_update
153
+ FileUtils.mkdir_p(@release.path)
154
+ older = Proxy::Omaha::Release.new(
155
+ :track => :stable,
156
+ :architecture => 'amd64-usr',
157
+ :version => '100.0.0'
158
+ )
159
+ FileUtils.mkdir_p(older.path)
160
+ older.mark_as_current!
161
+ assert older.current?
162
+ refute @release.current?
163
+ @release.mark_as_current!
164
+ assert @release.current?
165
+ refute older.current?
166
+ end
167
+
168
+ def test_digests_valid
169
+ FileUtils.mkdir_p(@release.path)
170
+ File.open(File.join(@release.path, "update.gz"), 'w') { |file| file.write('body') }
171
+ File.open(File.join(@release.path, "update.gz.DIGESTS"), 'w') { |file| file.write("841a2d689ad86bd1611447453c22c6fc update.gz\n") }
172
+ expected = {
173
+ 'update.gz' => ['841a2d689ad86bd1611447453c22c6fc']
174
+ }
175
+ assert_equal expected, @release.digests
176
+ assert_equal true, @release.valid?
177
+ end
178
+
179
+ def test_digests_invalid
180
+ FileUtils.mkdir_p(@release.path)
181
+ File.open(File.join(@release.path, "update.gz"), 'w') { |file| file.write('invalid') }
182
+ File.open(File.join(@release.path, "update.gz.DIGESTS"), 'w') { |file| file.write("841a2d689ad86bd1611447453c22c6fc update.gz\n") }
183
+ assert_equal false, @release.valid?
184
+ end
185
+
186
+ private
187
+
188
+ def expected_release_files
189
+ [
190
+ 'coreos_production_pxe.vmlinuz',
191
+ 'coreos_production_pxe.DIGESTS',
192
+ 'coreos_production_pxe_image.cpio.gz',
193
+ 'coreos_production_pxe_image.cpio.gz.DIGESTS',
194
+ 'coreos_production_image.bin.bz2',
195
+ 'coreos_production_image.bin.bz2.sig',
196
+ 'coreos_production_image.bin.bz2.DIGESTS',
197
+ 'coreos_production_vmware_raw_image.bin.bz2',
198
+ 'coreos_production_vmware_raw_image.bin.bz2.sig',
199
+ 'coreos_production_vmware_raw_image.bin.bz2.DIGESTS',
200
+ 'update.gz',
201
+ 'version.txt',
202
+ 'version.txt.DIGESTS'
203
+ ]
204
+ end
120
205
  end
@@ -12,7 +12,12 @@ class SyncerTest < Test::Unit::TestCase
12
12
  true
13
13
  end
14
14
 
15
+ def complete?
16
+ true
17
+ end
18
+
15
19
  def create; end
20
+ def mark_as_current!; end
16
21
  end
17
22
 
18
23
  class FakeReleaseProvider
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+ require 'smart_proxy_omaha/track'
3
+
4
+ class TrackTest < Test::Unit::TestCase
5
+
6
+ def test_valid
7
+ assert_equal true, Proxy::Omaha::Track.valid?('alpha')
8
+ end
9
+
10
+ def test_invalid
11
+ assert_equal false, Proxy::Omaha::Track.valid?('bär')
12
+ end
13
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_omaha
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timo Goebel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-24 00:00:00.000000000 Z
11
+ date: 2017-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -131,15 +131,18 @@ files:
131
131
  - lib/smart_proxy_omaha/http_download.rb
132
132
  - lib/smart_proxy_omaha/http_request.rb
133
133
  - lib/smart_proxy_omaha/http_shared.rb
134
+ - lib/smart_proxy_omaha/http_verify.rb
134
135
  - lib/smart_proxy_omaha/metadata.rb
135
136
  - lib/smart_proxy_omaha/metadata_provider.rb
136
137
  - lib/smart_proxy_omaha/omaha_api.rb
137
138
  - lib/smart_proxy_omaha/omaha_http_config.ru
138
139
  - lib/smart_proxy_omaha/omaha_plugin.rb
139
140
  - lib/smart_proxy_omaha/omaha_protocol.rb
141
+ - lib/smart_proxy_omaha/omaha_protocol/errorinternalresponse.rb
140
142
  - lib/smart_proxy_omaha/omaha_protocol/eventacknowledgeresponse.rb
141
143
  - lib/smart_proxy_omaha/omaha_protocol/handler.rb
142
144
  - lib/smart_proxy_omaha/omaha_protocol/noupdateresponse.rb
145
+ - lib/smart_proxy_omaha/omaha_protocol/pingresponse.rb
143
146
  - lib/smart_proxy_omaha/omaha_protocol/request.rb
144
147
  - lib/smart_proxy_omaha/omaha_protocol/response.rb
145
148
  - lib/smart_proxy_omaha/omaha_protocol/updateresponse.rb
@@ -147,13 +150,17 @@ files:
147
150
  - lib/smart_proxy_omaha/release_provider.rb
148
151
  - lib/smart_proxy_omaha/release_repository.rb
149
152
  - lib/smart_proxy_omaha/syncer.rb
153
+ - lib/smart_proxy_omaha/track.rb
150
154
  - lib/smart_proxy_omaha/version.rb
151
155
  - settings.d/omaha.yml.example
152
156
  - smart_proxy_omaha.gemspec
157
+ - test/fixtures/request_ping.xml
153
158
  - test/fixtures/request_update_complete_error.xml
154
159
  - test/fixtures/request_update_complete_noupdate.xml
155
160
  - test/fixtures/request_update_complete_update.xml
156
161
  - test/fixtures/request_update_download_started.xml
162
+ - test/fixtures/response_errorinternal.xml
163
+ - test/fixtures/response_ping.xml
157
164
  - test/fixtures/response_update_complete_error.xml
158
165
  - test/fixtures/response_update_complete_noupdate.xml
159
166
  - test/fixtures/response_update_complete_update.xml
@@ -164,8 +171,10 @@ files:
164
171
  - test/omaha/omaha_api_test.rb
165
172
  - test/omaha/omaha_protocol/request_test.rb
166
173
  - test/omaha/release_provider_test.rb
174
+ - test/omaha/release_repository_test.rb
167
175
  - test/omaha/release_test.rb
168
176
  - test/omaha/syncer_test.rb
177
+ - test/omaha/track_test.rb
169
178
  - test/test_helper.rb
170
179
  homepage: http://github.com/theforeman/smart_proxy_omaha
171
180
  licenses: