upman-daemon 0.0.8.4 → 0.0.8.5

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
  SHA256:
3
- metadata.gz: a5c7ac163d0dd1ba6132fb4a7c3c77a9e5131b20bd158d473de4f576ad858583
4
- data.tar.gz: 88631e841be85e59c2f8a00e13028771d4eaf03ff4142e4d199852275390d2c8
3
+ metadata.gz: b51d88026c608d0d4eb752780f0d44c611789e8cbf177fc818800a1447121249
4
+ data.tar.gz: b9d3627a649d7e69f4b012ebd0a3328d63957e59c115396e2c4d06832d260645
5
5
  SHA512:
6
- metadata.gz: a2768d6f096fac1b3a43cd5fe159afa2a1c906a0b08ae61ebc6855d6475433f2d9d3c98815ceafe6ab32413ef75af64bf66b6f0800034596b918024aa05ac3ac
7
- data.tar.gz: 9a14dc77f12af966feb76523faf26ef2fdd114d0fa8daafcce3bc0c0055f7c7974152bb542c0be96d1f302406302c72c93887f231f7ac5b5688f1f6a5382eb4a
6
+ metadata.gz: eeb4c315bbc741bd15dbe860f4546255f774b5263a93fcc864e2d04cdbb330346b96bee953dbbf4fdee7bea18821a74abb27746eff68a7bac3a2e55197affef3
7
+ data.tar.gz: 3ae68734a94fb756a63a0cc2cf6576a8593401e63d67b3ef0382798e7ee25b017b7bd67b897ba9bc871060edb462bc4264f181f0ada64c24bdfbc0d1712bc551
data/lib/upman.rb CHANGED
@@ -15,10 +15,15 @@ require_relative 'upman/core/config'
15
15
  require_relative 'upman/core/extension_base'
16
16
  require_relative 'upman/core/worker'
17
17
 
18
+ require_relative 'upman/services/node'
19
+ require_relative 'upman/services/install_history'
20
+ require_relative 'upman/services/installed_packages'
18
21
 
19
22
  require_relative 'upman/server/socket'
20
23
  require_relative 'upman/server/base_servlet'
21
24
 
25
+ require_relative 'upman/worker/base_worker'
26
+
22
27
  module Upman
23
28
  def self.run!(options)
24
29
  config_file = nil
@@ -1,50 +1,17 @@
1
- module Upman
2
- module Core
3
- class Register
4
- include ::Upman::Utils::Helper
5
- using SymbolizeHelper
6
- require 'securerandom'
7
-
8
- require 'facter'
9
- attr_accessor :hostdata_file
10
-
11
- def initialize
12
- @hostdata_file = 'hostdata.yml'
13
- end
14
-
15
- def perform
16
- data = generate_host_data
17
- info "Register Daemon running on #{data[:hostname]} with foreman_upman"
18
-
19
- api = ::Upman::Utils::Api.new
20
- api.post("upman/node/register", data.to_json)
21
- end
22
-
23
- def generate_host_data
24
- unless File.exist? @hostdata_file
25
- data = {
26
- :hostname => Socket.gethostbyname(Socket.gethostname).first,
27
- :uuid => SecureRandom.uuid,
28
- }
29
- File.write(@hostdata_file, data.to_yaml)
30
- end
31
- update_node_data
32
- end
33
-
34
- def update_node_data
35
- if File.exist? @hostdata_file
36
- data = YAML::load_file(@hostdata_file).deep_symbolize_keys
37
- data.merge!({
38
- :operatingsystem => Facter['operatingsystem'].value,
39
- :rubyversion => Facter['rubyversion'].value,
40
- :operatingsystemrelease => Facter['operatingsystemrelease'].value,
41
- :last_sync => DateTime.now
42
- })
43
- File.write(@hostdata_file, data.to_yaml)
44
- end
45
- data
46
- end
47
-
48
- end
49
- end
1
+ module Upman
2
+ module Core
3
+ class Register
4
+ include ::Upman::Utils::Helper
5
+
6
+ def perform
7
+ node_service = Upman::Service::Node.new
8
+ data = node_service.generate_node_data
9
+ info "Register Daemon running on #{data[:hostname]} with foreman_upman"
10
+
11
+ api = ::Upman::Utils::Api.new
12
+ api.post("upman/node/register", data.to_json)
13
+ end
14
+
15
+ end
16
+ end
50
17
  end
@@ -3,33 +3,50 @@ module Upman
3
3
  class Worker
4
4
 
5
5
  include Upman::Utils::Helper
6
+ include Upman::Utils::Dynload
7
+
8
+ attr_accessor :workers, :worker_threads
6
9
 
7
10
  def initialize
8
11
  @stop_signal = true
12
+ @config = ::Upman::Core::Config.daemon
13
+ @workers = []
14
+ @worker_threads = []
15
+
16
+ register_worker
9
17
  end
10
18
 
11
19
  def run!
12
- _banner
13
- while @stop_signal
14
- self.perform
15
- sleep(::Upman::Core::Config.daemon[:interval])
16
- end
20
+ show_banner
17
21
 
22
+ @workers.each do |worker|
23
+ @worker_threads << Thread.new do
24
+ worker.run!
25
+ end
26
+ end
27
+ @worker_threads.each { |t| t.join }
18
28
  end
19
29
 
20
30
  def shutdown
21
- info "Stopping working thread"
31
+ info "Stopping Worker Threads"
22
32
  @stop_signal = false
23
33
  end
24
34
 
25
35
 
26
- def perform
27
- info "Doing some work"
36
+ def register_worker
37
+ @config[:workers].each do |worker|
38
+ require_relative "../../upman/worker/#{worker}"
39
+ ext_obj = dynload("Upman::Worker::#{worker.split('_').map(&:capitalize).join('')}")
40
+ info "Register Worker Thread Upman::Worker::#{worker.split('_').map(&:capitalize).join('')}"
41
+ ext_class = ext_obj .new
42
+ @workers.append ext_class.register
43
+ end
28
44
  end
29
45
 
46
+
30
47
  private
31
48
 
32
- def _banner
49
+ def show_banner
33
50
  info "Running UpMan #{::Upman::Version::VERSION}"
34
51
  end
35
52
 
@@ -23,6 +23,11 @@ module Upman
23
23
 
24
24
  super(request, response)
25
25
 
26
+ unless is_authenticated(request)
27
+ response = not_authorized(response, "Request declined")
28
+ return response
29
+ end
30
+
26
31
  unless (@param_since = get_param('since', 'date', false))
27
32
  return nil
28
33
  end
@@ -33,56 +38,10 @@ module Upman
33
38
  end
34
39
 
35
40
  def perform_action
36
- result = []
37
- i = 0
38
- hist_lines = fetch_history.split("\n")
39
- hist_lines.each do |history|
40
- fill_up(result, i, hist_lines, history) if i.even?
41
- i += 1
42
- end
43
- result.to_json
44
- end
45
-
46
- private
47
-
48
- def fetch_history
49
- command = '(zcat $(ls -tr /var/log/apt/history.log*.gz); '\
50
- 'cat /var/log/apt/history.log) ' \
51
- '2>/dev/null | egrep \'^(Start-Date:|Commandline:)\' | '\
52
- 'grep -v aptdaemon'
53
- `#{command}`
41
+ install_history_service = ::Upman::Service::InstallHistory.new
42
+ install_history_service.get(@param_since).to_json
54
43
  end
55
44
 
56
- def fill_up(result, line_index, hist_lines, history)
57
- line = hist_lines[line_index + 1].sub('Commandline: ', '')
58
- command = parse_command(line)
59
- return nil if command.empty?
60
-
61
- command[:start_date] = DateTime.parse(history.sub('Start-Date: ', ''))
62
-
63
- if @param_since.instance_of?(Date)
64
- if command[:start_date] >= @param_since
65
- result.append command
66
- end
67
- else
68
- result.append command
69
- end
70
- end
71
-
72
- def parse_command(command_line)
73
- package = {}
74
- regexp = /(apt(-get)?)(( [\-\w]*)*) (install|upgrade|purge)(.*)?/
75
- if (matches = command_line.match(regexp))
76
- package[:action] = matches[5].strip
77
-
78
- package[:options] = []
79
- matches[3].split(' ').each { |name| package[:options].append name }
80
-
81
- package[:packages] = []
82
- matches[6].split(' ').each { |name| package[:packages].append name }
83
- end
84
- package
85
- end
86
45
  end
87
46
  end
88
47
  end
@@ -11,14 +11,17 @@ module Upman
11
11
 
12
12
  attr_accessor :requested
13
13
 
14
- include ::Upman::Utils::Parser
15
-
16
14
  # rubocop:disable Naming/MethodName
17
15
  def do_GET(request, response)
18
16
  # rubocop:enable Naming/MethodName
19
17
 
20
18
  super(request, response)
21
19
 
20
+ unless is_authenticated(request)
21
+ response = not_authorized(response, "Request declined")
22
+ return response
23
+ end
24
+
22
25
  @requested = get_param('requested', 'int')
23
26
 
24
27
  if (body = perform_action)
@@ -27,20 +30,8 @@ module Upman
27
30
  end
28
31
 
29
32
  def perform_action
30
- result = []
31
- extended_states = File.open('/var/lib/apt/extended_states', 'rb', &:read)
32
- extended_states.split("\n\n").each do |chunk|
33
- installed_packages = _get_hashed_values(chunk)
34
- unless @requested.empty?
35
- if installed_packages['auto_installed'] == @requested
36
- result.append installed_packages
37
- end
38
- else
39
- result.append installed_packages
40
- end
41
-
42
- end
43
- result.to_json
33
+ installed_package_service = Upman::Service::InstalledPackages.new
34
+ installed_package_service.get(@requested).to_json
44
35
  end
45
36
 
46
37
  end
@@ -8,17 +8,33 @@ module Upman
8
8
  return Servlet
9
9
  end
10
10
 
11
- class Servlet < WEBrick::HTTPServlet::AbstractServlet
12
- def do_GET request, response
13
- response.status = 200
14
- response['Content-Type'] = 'application/json'
15
- response.body = self.perform_action
16
- end
11
+ # Servlet action for WEBrick
12
+ class Servlet < ::Upman::Server::BaseServlet
13
+
14
+ attr_accessor :payload
15
+
16
+ # rubocop:disable Naming/MethodName
17
+ def do_GET(request, response)
18
+ # rubocop:enable Naming/MethodName
19
+
20
+ super(request, response)
17
21
 
22
+ unless is_authenticated(request)
23
+ response = not_authorized(response, "Request declined")
24
+ return response
25
+ end
26
+
27
+ @payload = get_param('payload', 'json', false)
28
+
29
+ if (body = perform_action)
30
+ response = ok(response, body)
31
+ end
32
+ end
18
33
 
19
34
  def perform_action
20
- "installPackage"
35
+ "{\"autentication\": \"failed - no valid certifacte was given\"}"
21
36
  end
37
+
22
38
  end
23
39
 
24
40
  end
@@ -15,6 +15,11 @@ module Upman
15
15
 
16
16
  super(request, response)
17
17
 
18
+ unless is_authenticated(request)
19
+ response = not_authorized(response, "Request declined")
20
+ return response
21
+ end
22
+
18
23
  response = ok(response, perform_action)
19
24
  end
20
25
 
@@ -1,65 +1,84 @@
1
- module Upman
2
- module Server
3
- class BaseServlet < WEBrick::HTTPServlet::AbstractServlet
4
-
5
- include ::Upman::Utils::Helper
6
-
7
- attr_accessor :query, :response, :error_msg
8
-
9
- def initialize(server, *options)
10
- super(server, *options)
11
- end
12
-
13
- # rubocop:disable Naming/MethodName
14
- def do_GET(_request, response)
15
- # rubocop:enable Naming/MethodName
16
-
17
- info "Process API request #{_request.path}"
18
-
19
- @response = response
20
- @query = _request.query
21
- end
22
-
23
- def ok(response, body)
24
- response.status = 200
25
- response['Content-Type'] = 'application/json'
26
- response.body = body
27
- response
28
- end
29
-
30
- def bad_request(response, body)
31
- response.status = 400
32
- response['Content-Type'] = 'application/json'
33
- response.body = "{\"status\": \"bad_request\", \"message\": \"#{body}\"}"
34
- response
35
- end
36
-
37
- def get_param(key, validate, require = false)
38
- if @query.nil?
39
- return ''
40
- end
41
-
42
- unless @query.include? key
43
- unless require
44
- return ''
45
- else
46
- return nil
47
- end
48
- end
49
- unless validate.nil?
50
- if validate == 'date'
51
- begin
52
- @query[key] = DateTime.parse(@query[key])
53
- rescue
54
- msg = "Invalid content for param #{key}, expected type: date - found #{@query[key]}"
55
- bad_request(@response, msg)
56
- fail msg
57
- return false
58
- end
59
- end
60
- end
61
- @query[key]
62
- end
63
- end
64
- end
1
+ module Upman
2
+ module Server
3
+ class BaseServlet < WEBrick::HTTPServlet::AbstractServlet
4
+
5
+ include ::Upman::Utils::Helper
6
+
7
+ attr_accessor :query, :response, :error_msg
8
+
9
+ def initialize(server, *options)
10
+ super(server, *options)
11
+ end
12
+
13
+ # rubocop:disable Naming/MethodName
14
+ def do_GET(_request, response)
15
+ # rubocop:enable Naming/MethodName
16
+
17
+ info "Process API request #{_request.path}"
18
+
19
+ @response = response
20
+ @query = _request.query
21
+ end
22
+
23
+ def is_authenticated(request)
24
+ node_service = Upman::Service::Node.new
25
+ node_uuid = node_service.get_node_uuuid
26
+
27
+ info "Checking Authentication"
28
+ return true if request["x-upman-token"] == node_uuid
29
+
30
+ fail "Access denied"
31
+
32
+ false
33
+ end
34
+
35
+ def ok(response, body)
36
+ response.status = 200
37
+ response['Content-Type'] = 'application/json'
38
+ response.body = body
39
+ response
40
+ end
41
+
42
+ def not_authorized(response, body)
43
+ response.status = 401
44
+ response['Content-Type'] = 'application/json'
45
+ response.body = "{\"status\": \"not_authorized\", \"message\": \"#{body}\"}"
46
+ response
47
+ end
48
+
49
+ def bad_request(response, body)
50
+ response.status = 400
51
+ response['Content-Type'] = 'application/json'
52
+ response.body = "{\"status\": \"bad_request\", \"message\": \"#{body}\"}"
53
+ response
54
+ end
55
+
56
+ def get_param(key, validate, require = false)
57
+ if @query.nil?
58
+ return ''
59
+ end
60
+
61
+ unless @query.include? key
62
+ unless require
63
+ return ''
64
+ else
65
+ return nil
66
+ end
67
+ end
68
+ unless validate.nil?
69
+ if validate == 'date'
70
+ begin
71
+ @query[key] = DateTime.parse(@query[key])
72
+ rescue
73
+ msg = "Invalid content for param #{key}, expected type: date - found #{@query[key]}"
74
+ bad_request(@response, msg)
75
+ fail msg
76
+ return false
77
+ end
78
+ end
79
+ end
80
+ @query[key]
81
+ end
82
+ end
83
+ end
65
84
  end
@@ -0,0 +1,54 @@
1
+ module Upman
2
+ module Service
3
+ class InstallHistory
4
+
5
+ attr_accessor :since
6
+
7
+ def get(since)
8
+ @since = since
9
+
10
+ result = []
11
+
12
+ lines = fetch_history.split("\n")
13
+ lines.each do |line|
14
+ fill_up(result, line)
15
+ end
16
+ result.reverse!
17
+ end
18
+
19
+ private
20
+
21
+ def fetch_history
22
+ command = 'for x in $(ls -1t /var/log/dpkg.log*); do zcat -f $x |tac |grep -e " install " -e " upgrade " -e " remove "; done |awk -F ":a" \'{print $1 " :a" $2}\' |column -t'
23
+ `#{command}`
24
+ end
25
+
26
+ def fill_up(result, line)
27
+ command = parse_command(line)
28
+ return nil if command.empty?
29
+
30
+ if @since.instance_of?(DateTime)
31
+ if DateTime.parse(command[:datetime]) >= @since
32
+ result.append command
33
+ end
34
+ else
35
+ result.append command
36
+ end
37
+ end
38
+
39
+ def parse_command(command_line)
40
+ package = {}
41
+ regexp = /([0-9]{4}-[0-9]{2}-[0-9]{2})\s+([0-9]{2}:[0-9]{2}:[0-9]{2})\s+(upgrade|install|remove)\s+([\w\-+\.]+)\s+\:([a-z0-9]+)\s+([\w+:\.\-~<>]+)\s+([\w+:\.\-~<>]+)/
42
+ unless !(matches = command_line.match(regexp))
43
+ package[:datetime] = "#{matches[1]} #{matches[2]}"
44
+ package[:action] = matches[3]
45
+ package[:package] = matches[4]
46
+ package[:architecture] = matches[5]
47
+ package[:old_version] = (%w[<keine> <none>].include? matches[6]) ? nil : matches[6]
48
+ package[:target_version] = (%w[<keine> <none>].include? matches[7]) ? nil : matches[7]
49
+ end
50
+ package
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ module Upman
2
+ module Service
3
+ class InstalledPackages
4
+
5
+ include ::Upman::Utils::Parser
6
+
7
+ def get(auto_installed)
8
+ result = []
9
+ extended_states = File.open('/var/lib/apt/extended_states', 'rb', &:read)
10
+ extended_states.split("\n\n").each do |chunk|
11
+ installed_packages = _get_hashed_values(chunk)
12
+ if auto_installed.empty?
13
+ result.append installed_packages
14
+ else
15
+ if installed_packages['auto_installed'] == auto_installed
16
+ result.append installed_packages
17
+ end
18
+ end
19
+ end
20
+ result
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,49 @@
1
+ module Upman
2
+ module Service
3
+ class Node
4
+
5
+ require 'securerandom'
6
+ require 'facter'
7
+ using SymbolizeHelper
8
+
9
+ attr_accessor :node_config
10
+
11
+ def initialize
12
+ @node_config = 'node.yml'
13
+ end
14
+
15
+
16
+ def get_node_uuuid
17
+ data = YAML::load_file(@node_config).deep_symbolize_keys
18
+ data[:uuid]
19
+ end
20
+
21
+ def generate_node_data
22
+ unless File.exist? @node_config
23
+ data = {
24
+ hostname: Socket.gethostbyname(Socket.gethostname).first,
25
+ uuid: SecureRandom.uuid,
26
+ }
27
+ File.write(@node_config, data.to_yaml)
28
+ end
29
+ update_node_data
30
+ end
31
+
32
+ def update_node_data
33
+ unless File.exist? @node_config
34
+ generate_node_data
35
+ end
36
+
37
+ data = YAML::load_file(@node_config).deep_symbolize_keys
38
+ data.merge!(
39
+ operatingsystem: Facter['operatingsystem'].value,
40
+ rubyversion: Facter['rubyversion'].value,
41
+ operatingsystemrelease: Facter['operatingsystemrelease'].value,
42
+ last_sync: DateTime.now
43
+ )
44
+ File.write(@node_config, data.to_yaml)
45
+ data
46
+ end
47
+ end
48
+ end
49
+ end
@@ -10,12 +10,12 @@ module Upman
10
10
  def check_connection
11
11
  response = self.get "status"
12
12
  if response.nil?
13
- false
13
+ return false
14
14
  end
15
15
 
16
16
  unless response.code == 200
17
17
  fail "Could not connect to API endpoint"
18
- false
18
+ return false
19
19
  end
20
20
  success "API Endpoint connected to Forman #{JSON.parse(response.body)["version"]} using API v#{JSON.parse(response.body)["api_version"]}."
21
21
  true
@@ -36,6 +36,8 @@ module Upman
36
36
  fail "Could not establish a secure connection to our API Endpoint"
37
37
  rescue RestClient::NotFound
38
38
  fail "API Endpoint route could not found"
39
+ rescue RestClient::InternalServerError
40
+ fail "API Endpoint reports error, ignore request"
39
41
  rescue Errno::ECONNREFUSED
40
42
  fail "Can not connect to Foreman instance on #{_url}"
41
43
  end
@@ -52,6 +54,8 @@ module Upman
52
54
  fail "Could not establish a secure connection to our API Endpoint"
53
55
  rescue RestClient::NotFound
54
56
  fail "API Endpoint route could not found"
57
+ rescue RestClient::InternalServerError
58
+ fail "API Endpoint reports error, ignore request"
55
59
  rescue Errno::ECONNREFUSED
56
60
  fail "Can not connect to your Foreman instance on #{_url}"
57
61
  end
@@ -0,0 +1,17 @@
1
+ module Upman
2
+ module Worker
3
+ class BaseWorker
4
+
5
+ include ::Upman::Utils::Helper
6
+
7
+ def run!
8
+ raise "No run! method defined"
9
+ end
10
+
11
+ def register
12
+ raise "No register method defined"
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module Upman
2
+ module Worker
3
+ class ReportInstallHistory < BaseWorker
4
+
5
+ def register
6
+ return self
7
+ end
8
+
9
+ def run!
10
+ while true
11
+ install_history_service = ::Upman::Service::InstallHistory.new
12
+ data = install_history_service.get("")
13
+
14
+ info "Report install history to API"
15
+
16
+
17
+ node_service = Upman::Service::Node.new
18
+ node_uuid = node_service.get_node_uuuid
19
+
20
+ api = ::Upman::Utils::Api.new
21
+ api.post("upman/node/install_history?uuid=#{node_uuid}", "{\"data\": #{data.to_json}}")
22
+
23
+ sleep(::Upman::Core::Config.daemon[:interval])
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ module Upman
2
+ module Worker
3
+ class ReportInstalledPackages < BaseWorker
4
+
5
+ def register
6
+ return self
7
+ end
8
+
9
+ def run!
10
+ while true
11
+ installed_package_service = ::Upman::Service::InstalledPackages.new
12
+ data = installed_package_service.get("1")
13
+ info "Report installed packages to API"
14
+
15
+
16
+ node_service = Upman::Service::Node.new
17
+ node_uuid = node_service.get_node_uuuid
18
+
19
+ #api = ::Upman::Utils::Api.new
20
+ #api.post("upman/node/installed_packages?uuid=#{node_uuid}","{\"data\": #{data.to_json}}")
21
+
22
+ sleep(::Upman::Core::Config.daemon[:interval])
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Upman
2
2
  class Version
3
- VERSION = '0.0.8.4'
3
+ VERSION = '0.0.8.5'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upman-daemon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8.4
4
+ version: 0.0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Ehrig
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-21 00:00:00.000000000 Z
11
+ date: 2019-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: coderay
@@ -97,12 +97,18 @@ files:
97
97
  - lib/upman/extensions/ping.rb
98
98
  - lib/upman/server/base_servlet.rb
99
99
  - lib/upman/server/socket.rb
100
+ - lib/upman/services/install_history.rb
101
+ - lib/upman/services/installed_packages.rb
102
+ - lib/upman/services/node.rb
100
103
  - lib/upman/utils/api.rb
101
104
  - lib/upman/utils/dynload.rb
102
105
  - lib/upman/utils/files.rb
103
106
  - lib/upman/utils/helper.rb
104
107
  - lib/upman/utils/parser.rb
105
108
  - lib/upman/utils/symbolize_helper.rb
109
+ - lib/upman/worker/base_worker.rb
110
+ - lib/upman/worker/report_install_history.rb
111
+ - lib/upman/worker/report_installed_packages.rb
106
112
  - lib/version.rb
107
113
  - upman.gemspec
108
114
  homepage: https://github.com/liKe2k1/upman