nexus_client 0.3.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.
data/README.md ADDED
@@ -0,0 +1,129 @@
1
+ [![Build Status](https://travis-ci.org/logicminds/nexus-client.svg)](https://travis-ci.org/logicminds/nexus-client)
2
+
3
+
4
+ # NexusClient
5
+ The nexus client is a ruby wrapper around the nexus REST API for downloading maven artifacts.
6
+ It features the ability to cache artifacts and also performs artifact checksums to ensure you only
7
+ download the artifact once. This gem was originally designed for use with configuration management software like puppet.
8
+
9
+ This gem does not require maven or any of the maven settings files. It was originally created to use with
10
+ configuration management software to download artifacts to hundreds of servers. A CLI tool was created for this purpose
11
+ to ease downloading of artifacts on any system.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'nexus_client'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install nexus_client
26
+
27
+ ## Features
28
+ ### Cache repository
29
+ The cache repository feature if enabled can be used to cache artifacts much like maven. However since this
30
+ gem does not use maven you are free to store the artifacts where ever you want. This works great for downloading the same
31
+ artifact on the same system. However, the cache feature was built to be used across a shared file system like NFS
32
+ so if multiple systems are downloading the same artifact you can reduce the time and bandwidth needed to download large
33
+ artifacts. This feature alone is like having a mini nexus proxy on your network!
34
+
35
+ ### Automatic artifact checksums
36
+ This gem will grab the sha1 checksum from the nexus server and compare the checksum
37
+ with the downloaded artifact. If the checksums are different and error will be raised. If the checksums match, a file
38
+ will be created with a extension of sha1 after the artifact is downloaded. This sha1 file additionally contains the sha1 checksum of the file.
39
+ This sha1 file is created as a trigger mechanism for configuration management software and also to speed up sha1 computation time of the artifact.
40
+ ```shell
41
+ Coreys-MacBook-Pro-2:tmp$ nexus-client --nexus-host https://repository.jboss.org/nexus -e -c /tmp/cache -g 'org.glassfish.main.external:ant:4.0:central::pom' -d /tmp
42
+ Coreys-MacBook-Pro-2:tmp$ ls -l
43
+ -rw-r--r-- 1 user1 wheel 8853 Oct 22 14:26 ant-4.0.pom
44
+ -rw-r--r-- 1 user1 wheel 40 Oct 22 14:26 ant-4.0.pom.sha1
45
+
46
+ Coreys-MacBook-Pro-2:tmp$ more ant-4.0.pom.sha1
47
+ 387951c0aa333024b25085d76a8ad77441b9e55f
48
+ ```
49
+
50
+ ### Smart Artifact Retrieval
51
+ This gem will use artifact checksums to ensure the artifact is only downloaded once. This
52
+ is really important during configuration management runtime when the artifact downloading process is expected run multiple times.
53
+ This is even more important when you use artifact snapshots. Artifacts that are snapshots can contain different checksums
54
+ at any time so its important that we download only when a new snapshot is detected by comparing the checksums.
55
+
56
+ ### Cache Analytics(Experimental)
57
+ This feature records the cache usage and shows just how much bandwidth has been saved and what
58
+ artifacts are currently cached. This feature is experimental and is not feature complete. It was originally designed
59
+ to show historically analysis of artifacts and make this information available to shell scripts, graphite and other reporting mechanisms.
60
+ This could be used later to send alerts when artifact sizes between versions/snapshots are significantly different from each other.
61
+
62
+
63
+ ## Ruby Usage
64
+
65
+ ```ruby
66
+ client = Nexus::Client.new
67
+ client.download_gav('/tmp/ant-4.0.pom', 'org.glassfish.main.external:ant:4.0:central::pom')
68
+ ```
69
+ or
70
+
71
+ ```ruby
72
+ Nexus::Client.download_gav('/tmp/ant-4.0.pom', 'org.glassfish.main.external:ant:4.0:central::pom')
73
+ ```
74
+
75
+ ## CLI Usage
76
+ We have also created a simple CLI tool that makes it easy to download artifacts from any nexus server.
77
+
78
+ ```shell
79
+ nexus-client --help
80
+ Options:
81
+ --destination, -d <s>: destination directory to download file to
82
+ --gav-string, -g <s>: The nexus GAV value: group:artifact:version:repository:classifier:extension
83
+ --cache-dir, -c <s>: The directory to cache files to
84
+ --enable-cache, -e: Enable cache
85
+ --nexus-host, -n <s>: Nexus host url, if left blank reads from ~/.nexus_host
86
+ --enable-analytics, -a: Enable cache analytics, requires sqlite3 gem (experimental!)
87
+ --help, -h: Show this message
88
+
89
+ nexus-client --nexus-host https://repository.jboss.org/nexus -e -c /tmp/cache -g 'org.glassfish.main.external:ant:4.0:central::pom' -d /tmp
90
+ ```
91
+
92
+ ## Tips
93
+
94
+ Create a nexus host file to store the nexus host url. This can same time if your nexus host url is the same. By default
95
+ the nexus-client CLI will override the stored url in your nexus_host file when passing in the --nexus-host argument.
96
+
97
+ ```shell
98
+ Coreys-MacBook-Pro-2:~$ pwd
99
+ /Users/user1
100
+ Coreys-MacBook-Pro-2: echo 'https://repository.jboss.org/nexus' > ~/.nexus_host
101
+ Coreys-MacBook-Pro-2:~$ more .nexus_host
102
+ https://repository.jboss.org/nexus
103
+ ```
104
+ ## TODO
105
+ * Extend usage to other supported maven server solutions (artifactory, archiva)
106
+ * Implement a feature toggle to enable parallel downloads.
107
+ * Finish analytics feature
108
+ * Remove usage of typheous and other C compiled dependent gems (conflicts with parallel feature)
109
+ * Add feature to upload artifacts to nexus server
110
+ * Add basic user authentication
111
+
112
+
113
+ ## OS Support
114
+ Should work on all *nix platforms. Windows may also be supported but it has never been tested.
115
+ If you use this gem on windows please let me know so I can update this doc.
116
+
117
+ ## Contributing
118
+
119
+ 1. Fork it
120
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
121
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
122
+ 4. Push to the branch (`git push origin my-new-feature`)
123
+ 5. Create new Pull Request
124
+
125
+
126
+ ## Examples
127
+
128
+
129
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ task :default => :spec
8
+
9
+ desc "Build gem"
10
+ task :build do
11
+ `gem build nexus_client.gemspec`
12
+ end
data/bin/nexus-client ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'nexus_client'
4
+ require 'trollop'
5
+ require 'etc'
6
+
7
+ # read host will read ~/.nexus_host file and
8
+ def read_host(filename="#{Etc.getpwuid.dir}/.nexus_host")
9
+ fn = File.expand_path(filename)
10
+ abort("Please create the file #{filename} and add your nexus host") if not File.exists?(filename)
11
+ begin
12
+ File.open(fn, 'r') { |f| f.read }.strip
13
+ rescue Exception => e
14
+ raise(e)
15
+ end
16
+ end
17
+
18
+ opts = Trollop::options do
19
+ opt :destination, "destination directory to download file to " , :type => :string, :required => true
20
+ opt :gav_string, "The nexus GAV value: group:artifact:version:repository:classifier:extension" , :type => :string, :required => true
21
+ opt :cache_dir, "The directory to cache files to" , :type => :string
22
+ opt :enable_cache, "Enable cache", :default => false, :type => :boolean
23
+ opt :nexus_host, "Nexus host url, if left blank reads from ~/.nexus_host", :type => :string
24
+ opt :enable_analytics, "Enable cache analytics, requires sqlite3 gem (experimental!)", :type => :boolean, :default => false
25
+ end
26
+
27
+ if opts[:nexus_host].nil? or opts[:nexus_host].empty?
28
+ opts[:nexus_host] = read_host
29
+ end
30
+
31
+ value = Nexus::Client.download(opts[:destination], opts[:gav_string],
32
+ opts[:cache_dir], opts[:enable_cache],
33
+ opts[:enable_analytics], opts[:nexus_host])
34
+
35
+ if value
36
+ exit(0)
37
+ end
38
+ exit(1)
@@ -0,0 +1,194 @@
1
+ require "nexus_client/version"
2
+ require "nexus_client/gav"
3
+ require 'nexus_client/cache'
4
+ require 'nexus_client/analytics'
5
+ require "tmpdir"
6
+ require 'typhoeus'
7
+ require 'json'
8
+ require 'etc'
9
+ require 'fileutils'
10
+
11
+ module Nexus
12
+ class Client
13
+ attr_reader :host, :cache
14
+ attr_accessor :use_cache, :log
15
+
16
+ def initialize(nexus_host=nil, cache_dir='/tmp/cache', enable_cache=true, enable_analytics=false,logger=nil)
17
+ @log = logger
18
+ @host = nexus_host || default_host
19
+ @host = @host.gsub(/\/nexus$/, '') # just in case user enters /nexus
20
+ @use_cache = enable_cache
21
+ if @use_cache
22
+ @cache_base = cache_dir
23
+ @cache = Nexus::Cache.new(@cache_base, enable_analytics, log)
24
+ end
25
+ #Typhoeus::Config.verbose = true
26
+
27
+ end
28
+
29
+ # read host will read ~/.nexus_host file and
30
+ def read_host(filename="#{Etc.getpwuid.dir}/.nexus_host")
31
+ fn = File.expand_path(filename)
32
+ abort("Please create the file #{filename} and add your nexus host") if not File.exists?(filename)
33
+ begin
34
+ File.open(fn, 'r') { |f| f.read }.strip
35
+ rescue Exception => e
36
+ raise(e)
37
+ end
38
+ end
39
+
40
+ def default_host
41
+ read_host
42
+ end
43
+
44
+ def log
45
+ if @log.nil?
46
+ @log = Logger.new(STDOUT)
47
+ end
48
+ @log
49
+ end
50
+
51
+ def self.download(destination, gav_str, cache_dir='/tmp/cache', enable_cache=false,enable_analytics=false,host=nil)
52
+ client = Nexus::Client.new(host, cache_dir, enable_cache,enable_analytics)
53
+ client.download_gav(destination, gav_str)
54
+ end
55
+
56
+ def download_gav(destination, gav_str)
57
+ gav = Nexus::Gav.new(gav_str)
58
+ download(destination, gav)
59
+ end
60
+
61
+ def create_target(destination)
62
+ destination = File.expand_path(destination)
63
+ if ! File.directory?(destination)
64
+ begin
65
+ FileUtils.mkdir_p(destination) if not File.exists?(destination)
66
+ rescue SystemCallError => e
67
+ raise e, 'Cannot create directory'
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ # retrieves the attributes of the gav
74
+ def gav_data(gav)
75
+ res = {}
76
+ request = Typhoeus::Request.new(
77
+ "#{host}/nexus/service/local/artifact/maven/resolve",
78
+ :params => gav.to_hash,:connecttimeout => 5,
79
+ :headers => { 'Accept' => 'application/json' }
80
+ )
81
+ request.on_failure do |response|
82
+ raise("Failed to get gav data for #{gav.to_s}")
83
+ end
84
+ request.on_complete do |response|
85
+ res = JSON.parse(response.response_body)
86
+ end
87
+ request.run
88
+
89
+ res['data']
90
+ end
91
+
92
+ # returns the sha1 of the file
93
+ def sha(file, use_sha_file=false)
94
+ if use_sha_file and File.exists?("#{file}.sha1")
95
+ # reading the file is faster than doing a hash, so we keep the hash in the file
96
+ # then we read back and compare. There is no reason to perform sha1 everytime
97
+ begin
98
+ File.open("#{file}.sha1", 'r') { |f| f.read().strip}
99
+ rescue
100
+ Digest::SHA1.file(File.expand_path(file)).hexdigest
101
+ end
102
+ else
103
+ Digest::SHA1.file(File.expand_path(file)).hexdigest
104
+ end
105
+ end
106
+
107
+ # sha_match? returns bool by comparing the sha1 of the nexus gav artifact and the local file
108
+ def sha_match?(file, gav, use_sha_file=false)
109
+ if File.exists?(file)
110
+ if gav.sha1.nil?
111
+ gav.sha1 = gav_data(gav)['sha1']
112
+ end
113
+ sha(file,use_sha_file) == gav.sha1
114
+ else
115
+ false
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ # writes the sha1 a file if and only if the contents of the file do not match
122
+ def write_sha1(file,sha1)
123
+ shafile = "#{file}.sha1"
124
+ File.open(shafile, 'w') { |f| f.write(sha1) }
125
+ end
126
+
127
+ # downloads the gav to the destination, returns the file if download was successful
128
+ # if cache is on then it will use the cache and if file is new will also cache the new file
129
+ # TODO need a timeout when host is unreachable
130
+ def download(destination, gav)
131
+ raise 'Download destination must not be empty' if destination.empty?
132
+ create_target(destination) # ensure directory path is created
133
+ destination = File.expand_path(destination)
134
+ if File.directory?(destination)
135
+ dstfile = File.expand_path("#{destination}/#{gav.filename}")
136
+ else
137
+ dstfile = File.expand_path(destination)
138
+ end
139
+ # if the file already exists at the destination path than we don't need to download it again
140
+ if sha_match?(dstfile, gav)
141
+ # create a file that stores the sha1 for faster file comparisions later
142
+ # This will only get created when the sha1 matches
143
+ write_sha1("#{dstfile}", gav.sha1)
144
+ if use_cache and not cache.exists?(gav)
145
+ cache.add_file(gav, dstfile)
146
+ end
147
+ return true
148
+ end
149
+ # remove the previous sha1 file if it already exists
150
+ FileUtils.rm("#{dstfile}.sha1") if File.exists?("#{dstfile}.sha1")
151
+
152
+ if gav.sha1.nil?
153
+ gav.sha1 = gav_data(gav)['sha1']
154
+ end
155
+ # use the cache if the file is in the cache
156
+ if use_cache and cache.exists?(gav)
157
+ cache_file_path = cache.file_path(gav)
158
+ FileUtils.copy(cache_file_path, dstfile)
159
+ cache.record_hit(gav)
160
+ else
161
+ request = Typhoeus::Request.new(
162
+ "#{host}/nexus/service/local/artifact/maven/redirect",
163
+ :params => gav.to_hash,
164
+ :connecttimeout => 5,
165
+ :followlocation => true
166
+ )
167
+ request.on_failure do |response|
168
+ raise("Failed to download #{gav.to_s}")
169
+ end
170
+
171
+ # when complete, lets write the data to the file
172
+ # first lets compare the sha matches
173
+ # if the gav we thought we downloaded has the same checksum, were are good
174
+ request.on_complete do |response|
175
+ File.open(dstfile, 'wb') { |f| f.write(response.body) } unless ! response.success?
176
+ if not sha_match?(dstfile, gav, false)
177
+ raise("Error sha1 mismatch gav #{gav.sha1} != #{sha(dstfile)}")
178
+ end
179
+ gav.attributes[:size] = File.size(dstfile)
180
+ gav.attributes[:total_time] = response.options[:total_time]
181
+
182
+ # lets cache the file if cache is on
183
+ if use_cache
184
+ cache.add_file(gav, dstfile)
185
+ end
186
+ dstfile
187
+ end
188
+ request.run
189
+ dstfile
190
+ end
191
+ dstfile
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,187 @@
1
+ require 'json'
2
+
3
+ module Nexus
4
+ class Analytics
5
+ attr_reader :db, :data, :db_file
6
+ attr_accessor :a_file, :log
7
+
8
+ # new method
9
+ def initialize(database_dir='./',db_filename='cache-analytics.db', logger=nil)
10
+ @log = logger
11
+
12
+ filename ||= db_filename || 'cache-analytics.db'
13
+
14
+ log.warn "Filename is nil" if filename.nil?
15
+ @db_file = File.join(database_dir,filename)
16
+ begin
17
+ require 'sqlite3'
18
+ @db = SQLite3::Database.new( @db_file)
19
+ init_tables
20
+ total_view
21
+ rescue LoadError => e
22
+ log.error 'The sqlite3 gem must be installed before using the analytics class'
23
+ raise(e.message)
24
+ end
25
+ end
26
+
27
+ def log
28
+ if @log.nil?
29
+ @log = Logger.new(STDOUT)
30
+ end
31
+ @log
32
+ end
33
+
34
+ def add_item(gav, file_path)
35
+ begin
36
+ db.execute("insert into artifacts (sha1,artifact_gav,filesize,request_time, modified, file_location) "+
37
+ "values ('#{gav.sha1}','#{gav.to_s}', #{gav.attributes[:size]}, #{gav.attributes[:total_time]},"+
38
+ "'#{Time.now.to_i}', '#{file_path}')")
39
+ rescue
40
+ log.warn("Ignoring Duplicate entry #{file_path}")
41
+ end
42
+
43
+ end
44
+
45
+ def gavs
46
+ db.execute("select artifact_gav from artifacts").flatten
47
+ end
48
+
49
+ def update_item(gav)
50
+ count = hit_count(gav)
51
+ db.execute <<SQL
52
+ UPDATE artifacts SET hit_count=#{count + 1}, modified=#{Time.now.to_i}
53
+ WHERE sha1='#{gav.sha1}'
54
+ SQL
55
+ end
56
+
57
+ def totals
58
+ db.execute("select * from totals")
59
+ end
60
+
61
+ def total(gav)
62
+ # TODO fix NoMethodError: undefined method `sha1' for #<String:0x7f1a0f387720>
63
+ # when type is not a gav or sha1 is not available
64
+ data = db.execute("select * from totals where sha1 = '#{gav.sha1}'")
65
+ db.results_as_hash = false
66
+ data
67
+ end
68
+
69
+
70
+ def hit_count(gav)
71
+ row = db.execute("select hit_count from totals where sha1 = '#{gav.sha1}'").first
72
+ if row.nil?
73
+ 0
74
+ else
75
+ row.first
76
+ end
77
+ end
78
+
79
+ def total_time(gav)
80
+ row = db.execute("select total_time_saved from totals where sha1 = '#{gav.sha1}'").first
81
+ if row.nil?
82
+ 0
83
+ else
84
+ row.first
85
+ end
86
+
87
+ end
88
+
89
+ def total_bytes(gav, pretty=false)
90
+ row = db.execute("select total_bytes_saved from totals where sha1 = '#{gav.sha1}'").first
91
+ if row.nil?
92
+ 0
93
+ else
94
+ if pretty
95
+ Filesize.from("#{row.first} B").pretty
96
+ else
97
+ row.first
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ # returns the totals view as json
104
+ # the results as hash returns extra key/values we don't want so
105
+ # we had to create our own hash
106
+ # there are better ways of doing this but this was simple to create
107
+ def to_json(pretty=true)
108
+ db.results_as_hash = false
109
+ totals = db.execute("select * from totals")
110
+ hash_total = []
111
+ totals.each do |row|
112
+ h = {}
113
+ (0...row.length).each do |col|
114
+ h[total_columns[col]] = row[col]
115
+ end
116
+ hash_total << h
117
+ end
118
+ if pretty
119
+ JSON.pretty_generate(hash_total)
120
+ else
121
+ hash_total.to_json
122
+ end
123
+ end
124
+
125
+ # removes old items from the database that are older than mtime
126
+ def remove_old_items(mtime)
127
+ db.execute <<SQL
128
+ DELETE from artifacts where modified < #{mtime}
129
+ SQL
130
+ end
131
+
132
+ # get items older than mtime, defaults to 2 days ago
133
+ def old_items(mtime=(Time.now.to_i)-(172800))
134
+ data = db.execute <<SQL
135
+ SELECT * from artifacts where modified < #{mtime}
136
+ SQL
137
+ data || []
138
+ end
139
+
140
+ # returns the top X most utilized caches
141
+ def top_x(amount=10)
142
+ db.execute <<SQL
143
+ SELECT * FROM totals
144
+ ORDER BY hit_count desc
145
+ LIMIT #{amount}
146
+ SQL
147
+ end
148
+
149
+ private
150
+
151
+ def total_columns
152
+ %w(sha1 artifact_gav filesize request_time modified file_location hit_count total_bytes_saved total_time_saved)
153
+ end
154
+
155
+ def artifact_columns
156
+ %w(sha1 artifact_gav filesize, request_time hit_count modified file_location )
157
+
158
+ end
159
+
160
+ def init_tables
161
+ # id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
162
+ db.execute <<SQL
163
+ CREATE TABLE IF NOT EXISTS artifacts (
164
+ sha1 VARCHAR(40) PRIMARY KEY NOT NULL,
165
+ artifact_gav VARCHAR(255),
166
+ filesize BIGINT default 0,
167
+ request_time FLOAT default 0,
168
+ hit_count INTEGER default 0,
169
+ modified BIGINT default '#{Time.now.to_i}',
170
+ file_location VARCHAR(255)
171
+ );
172
+ SQL
173
+ end
174
+
175
+ def total_view
176
+ db.execute <<SQL
177
+ CREATE VIEW IF NOT EXISTS totals AS
178
+ SELECT sha1, artifact_gav, filesize, request_time, modified, file_location,
179
+ hit_count, sum(hit_count * filesize) AS 'total_bytes_saved',
180
+ sum(hit_count * request_time) as 'total_time_saved'
181
+ FROM artifacts
182
+ SQL
183
+ end
184
+
185
+ end
186
+ end
187
+