nexus_client 0.3.0

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