vanagon 0.3.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +13 -0
  3. data/README.md +175 -0
  4. data/bin/build +33 -0
  5. data/bin/devkit +22 -0
  6. data/bin/repo +26 -0
  7. data/bin/ship +15 -0
  8. data/lib/vanagon.rb +8 -0
  9. data/lib/vanagon/common.rb +2 -0
  10. data/lib/vanagon/common/pathname.rb +87 -0
  11. data/lib/vanagon/common/user.rb +25 -0
  12. data/lib/vanagon/component.rb +157 -0
  13. data/lib/vanagon/component/dsl.rb +307 -0
  14. data/lib/vanagon/component/source.rb +66 -0
  15. data/lib/vanagon/component/source/git.rb +60 -0
  16. data/lib/vanagon/component/source/http.rb +158 -0
  17. data/lib/vanagon/driver.rb +112 -0
  18. data/lib/vanagon/engine/base.rb +82 -0
  19. data/lib/vanagon/engine/docker.rb +40 -0
  20. data/lib/vanagon/engine/local.rb +40 -0
  21. data/lib/vanagon/engine/pooler.rb +85 -0
  22. data/lib/vanagon/errors.rb +28 -0
  23. data/lib/vanagon/extensions/string.rb +11 -0
  24. data/lib/vanagon/optparse.rb +62 -0
  25. data/lib/vanagon/platform.rb +245 -0
  26. data/lib/vanagon/platform/deb.rb +71 -0
  27. data/lib/vanagon/platform/dsl.rb +293 -0
  28. data/lib/vanagon/platform/osx.rb +100 -0
  29. data/lib/vanagon/platform/rpm.rb +76 -0
  30. data/lib/vanagon/platform/rpm/wrl.rb +39 -0
  31. data/lib/vanagon/platform/solaris_10.rb +182 -0
  32. data/lib/vanagon/platform/solaris_11.rb +138 -0
  33. data/lib/vanagon/platform/swix.rb +35 -0
  34. data/lib/vanagon/project.rb +251 -0
  35. data/lib/vanagon/project/dsl.rb +218 -0
  36. data/lib/vanagon/utilities.rb +299 -0
  37. data/spec/fixures/component/invalid-test-fixture.json +3 -0
  38. data/spec/fixures/component/mcollective.service +1 -0
  39. data/spec/fixures/component/test-fixture.json +4 -0
  40. data/spec/lib/vanagon/common/pathname_spec.rb +103 -0
  41. data/spec/lib/vanagon/common/user_spec.rb +36 -0
  42. data/spec/lib/vanagon/component/dsl_spec.rb +443 -0
  43. data/spec/lib/vanagon/component/source/git_spec.rb +19 -0
  44. data/spec/lib/vanagon/component/source/http_spec.rb +43 -0
  45. data/spec/lib/vanagon/component/source_spec.rb +99 -0
  46. data/spec/lib/vanagon/component_spec.rb +22 -0
  47. data/spec/lib/vanagon/engine/base_spec.rb +40 -0
  48. data/spec/lib/vanagon/engine/docker_spec.rb +40 -0
  49. data/spec/lib/vanagon/engine/pooler_spec.rb +54 -0
  50. data/spec/lib/vanagon/platform/deb_spec.rb +60 -0
  51. data/spec/lib/vanagon/platform/dsl_spec.rb +128 -0
  52. data/spec/lib/vanagon/platform/rpm_spec.rb +41 -0
  53. data/spec/lib/vanagon/platform/solaris_11_spec.rb +44 -0
  54. data/spec/lib/vanagon/platform_spec.rb +53 -0
  55. data/spec/lib/vanagon/project/dsl_spec.rb +203 -0
  56. data/spec/lib/vanagon/project_spec.rb +44 -0
  57. data/spec/lib/vanagon/utilities_spec.rb +140 -0
  58. data/templates/Makefile.erb +116 -0
  59. data/templates/deb/changelog.erb +5 -0
  60. data/templates/deb/conffiles.erb +3 -0
  61. data/templates/deb/control.erb +21 -0
  62. data/templates/deb/dirs.erb +3 -0
  63. data/templates/deb/docs.erb +1 -0
  64. data/templates/deb/install.erb +3 -0
  65. data/templates/deb/postinst.erb +46 -0
  66. data/templates/deb/postrm.erb +15 -0
  67. data/templates/deb/prerm.erb +17 -0
  68. data/templates/deb/rules.erb +25 -0
  69. data/templates/osx/postinstall.erb +24 -0
  70. data/templates/osx/preinstall.erb +19 -0
  71. data/templates/osx/project-installer.xml.erb +19 -0
  72. data/templates/rpm/project.spec.erb +217 -0
  73. data/templates/solaris/10/depend.erb +3 -0
  74. data/templates/solaris/10/pkginfo.erb +13 -0
  75. data/templates/solaris/10/postinstall.erb +37 -0
  76. data/templates/solaris/10/preinstall.erb +7 -0
  77. data/templates/solaris/10/preremove.erb +6 -0
  78. data/templates/solaris/10/proto.erb +5 -0
  79. data/templates/solaris/11/p5m.erb +73 -0
  80. metadata +172 -0
@@ -0,0 +1,299 @@
1
+ require 'vanagon/errors'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'json'
5
+ require 'digest'
6
+ require 'erb'
7
+ require 'timeout'
8
+ require 'vanagon/extensions/string'
9
+
10
+ class Vanagon
11
+ module Utilities
12
+
13
+ # Utility to get the md5 sum of a file
14
+ #
15
+ # @param file [String] file to md5sum
16
+ # @return [String] md5sum of the given file
17
+ def get_md5sum(file)
18
+ Digest::MD5.file(file).hexdigest.to_s
19
+ end
20
+
21
+ # Generic file summing utility
22
+ #
23
+ # @param file [String] file to sum
24
+ # @param type [String] type of sum to provide
25
+ # @return [String] sum of the given file
26
+ # @raise [RuntimeError] raises an exception if the given sum type is not supported
27
+ def get_sum(file, type)
28
+ case type.downcase
29
+ when 'md5'
30
+ Digest::MD5.file(file).hexdigest.to_s
31
+ when 'sha512'
32
+ Digest::SHA512.file(file).hexdigest.to_s
33
+ else
34
+ fail "Don't know how to produce a sum of type: '#{type}' for '#{file}'."
35
+ end
36
+ end
37
+
38
+ # Simple wrapper around Net::HTTP. Will make a request of the given type to
39
+ # the given url and return the body as parsed by JSON.
40
+ #
41
+ # @param url [String] The url to make the request against (needs to be parsable by URI
42
+ # @param type [String] One of the supported request types (currently 'get', 'post', 'delete')
43
+ # @param payload [String] The request body data payload used for POST and PUT
44
+ # @param header [Hash] Send additional information in the HTTP request header
45
+ # @return [Hash] The response body is parsed by JSON and returned
46
+ # @raise [RuntimeError, Vanagon::Error] an exception is raised if the
47
+ # action is not supported, or if there is a problem with the http request,
48
+ # or if the response is not JSON
49
+ def http_request(url, type, payload = {}.to_json, header = nil)
50
+ uri = URI.parse(url)
51
+ http = Net::HTTP.new(uri.host, uri.port)
52
+
53
+ case type.downcase
54
+ when "get"
55
+ request = Net::HTTP::Get.new(uri.request_uri)
56
+ when "post"
57
+ request = Net::HTTP::Post.new(uri.request_uri)
58
+ request.body = payload
59
+ when "put"
60
+ request = Net::HTTP::Put.new(uri.request_uri)
61
+ request.body = payload
62
+ when "delete"
63
+ request = Net::HTTP::Delete.new(uri.request_uri)
64
+ else
65
+ fail "ACTION: #{type} not supported by #http_request method. Maybe you should add it?"
66
+ end
67
+
68
+ # Add any headers to the request
69
+ if header && header.is_a?(Hash)
70
+ header.each do |key, val|
71
+ request[key] = val
72
+ end
73
+ end
74
+
75
+ response = http.request(request)
76
+
77
+ JSON.parse(response.body)
78
+
79
+ rescue Errno::ETIMEDOUT, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
80
+ EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
81
+ Net::ProtocolError => e
82
+ raise Vanagon::Error.wrap(e, "Problem reaching #{url}. Is #{uri.host} down?")
83
+ rescue JSON::ParserError => e
84
+ raise Vanagon::Error.wrap(e, "#{uri.host} handed us a response that doesn't look like JSON.")
85
+ end
86
+
87
+ # Similar to rake's sh, the passed command will be executed and an
88
+ # exception will be raised on command failure. However, in contrast to
89
+ # rake's sh, this method returns the output of the command instead of a
90
+ # boolean.
91
+ #
92
+ # @param command [String] The command to be executed
93
+ # @return [String] The standard output of the executed command
94
+ # @raise [Vanagon::Error] If the command fails an exception is raised
95
+ def ex(command)
96
+ ret = `#{command}`
97
+ unless $?.success?
98
+ raise Vanagon::Error, "'#{command}' did not succeed"
99
+ end
100
+ ret
101
+ end
102
+
103
+ # Similar to the command-line utility which, the method will search the
104
+ # PATH for the passed command and return the full path to the command if it
105
+ # exists.
106
+ #
107
+ # @param command [String] Command to search for on PATH
108
+ # @param required [true, false] Whether or not to raise an exception if the command cannot be found
109
+ # @return [String, false] Returns either the full path to the command or false if the command cannot be found
110
+ # @raise [RuntimeError] If the command is required and cannot be found
111
+ def find_program_on_path(command, required = true)
112
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path_elem|
113
+ location = File.join(path_elem, command)
114
+ return location if FileTest.executable?(location)
115
+ end
116
+
117
+ if required
118
+ fail "Could not find '#{command}'. Please install (or ensure it is on $PATH), and try again."
119
+ else
120
+ return false
121
+ end
122
+ end
123
+
124
+ # Method to retry a ruby block and fail if the command does not succeed
125
+ # within the number of tries and timeout.
126
+ #
127
+ # @param tries [Integer] number of times to try calling the block
128
+ # @param timeout [Integer] number of seconds to run the block before timing out
129
+ # @return [true] If the block succeeds, true is returned
130
+ # @raise [Vanagon::Error] if the block fails after the retries are exhausted, an error is raised
131
+ def retry_with_timeout(tries = 5, timeout = 1, &blk)
132
+ tries.times do
133
+ Timeout::timeout(timeout) do
134
+ begin
135
+ blk.call
136
+ return true
137
+ rescue
138
+ warn 'An error was encountered evaluating block. Retrying..'
139
+ end
140
+ end
141
+ end
142
+
143
+ raise Vanagon::Error, "Block failed maximum of #{tries} tries. Exiting.."
144
+ end
145
+
146
+ # Simple wrapper around git command line executes the given commands and
147
+ # returns the results.
148
+ #
149
+ # @param commands [String] The commands to be run
150
+ # @return [String] The output of the command
151
+ def git(*commands)
152
+ git_bin = find_program_on_path('git')
153
+ %x(#{git_bin} #{commands.join(' ')})
154
+ end
155
+
156
+ # Determines if the given directory is a git repo or not
157
+ #
158
+ # @param directory [String] The directory to check
159
+ # @return [true, false] True if the directory is a git repo, false otherwise
160
+ def is_git_repo?(directory = Dir.pwd)
161
+ Dir.chdir(directory) do
162
+ git('rev-parse', '--git-dir', '> /dev/null 2>&1')
163
+ $?.success?
164
+ end
165
+ end
166
+
167
+ # Determines a version for the given directory based on the git describe
168
+ # for the repository
169
+ #
170
+ # @param directory [String] The directory to use in versioning
171
+ # @return [String] The version of the directory accoring to git describe
172
+ # @raise [RuntimeError] If the given directory is not a git repo
173
+ def git_version(directory = Dir.pwd)
174
+ if is_git_repo?(directory)
175
+ Dir.chdir(directory) do
176
+ version = git('describe', '--tags', '2> /dev/null').chomp
177
+ if version.empty?
178
+ warn "Directory '#{directory}' cannot be versioned by git. Maybe it hasn't been tagged yet?"
179
+ end
180
+ return version
181
+ end
182
+ else
183
+ fail "Directory '#{directory}' is not a git repo, cannot get a version"
184
+ end
185
+ end
186
+
187
+ # Sends the desired file/directory to the destination using rsync
188
+ #
189
+ # @param source [String] file or directory to send
190
+ # @param target [String] ssh host to send to (user@machine)
191
+ # @param dest [String] path on target to place the source
192
+ # @param extra_flags [Array] any additional flags to pass to rsync
193
+ # @param port [Integer] Port number for ssh (default 22)
194
+ # @return [String] output of rsync command
195
+ def rsync_to(source, target, dest, port = 22, extra_flags = ["--ignore-existing"])
196
+ rsync = find_program_on_path('rsync')
197
+ flags = "-rHlv --no-perms --no-owner --no-group"
198
+ unless extra_flags.empty?
199
+ flags << " " << extra_flags.join(" ")
200
+ end
201
+ ex("#{rsync} -e '#{ssh_command(port)}' #{flags} #{source} #{target}:#{dest}")
202
+ end
203
+
204
+ # Hacky wrapper to add on the correct flags for ssh to be used in ssh and rsync methods
205
+ #
206
+ # @param port [Integer] Port number for ssh (default 22)
207
+ # @return [String] start of ssh command, including flags for ssh keys
208
+ def ssh_command(port = 22)
209
+ ssh = find_program_on_path('ssh')
210
+ args = ENV['VANAGON_SSH_KEY'] ? " -i #{ENV['VANAGON_SSH_KEY']}" : ""
211
+ args << " -p #{port} "
212
+ args << " -o UserKnownHostsFile=/dev/null"
213
+ args << " -o StrictHostKeyChecking=no"
214
+ return ssh + args
215
+ end
216
+
217
+ # Retrieves the desired file/directory from the destination using rsync
218
+ #
219
+ # @param source [String] path on target to retrieve from
220
+ # @param target [String] ssh host to retrieve from (user@machine)
221
+ # @param dest [String] path on local host to place the source
222
+ # @param port [Integer] port number for ssh (default 22)
223
+ # @param extra_flags [Array] any additional flags to pass to rsync
224
+ # @return [String] output of rsync command
225
+ def rsync_from(source, target, dest, port = 22, extra_flags = [])
226
+ rsync = find_program_on_path('rsync')
227
+ flags = "-rHlv -O --no-perms --no-owner --no-group"
228
+ unless extra_flags.empty?
229
+ flags << " " << extra_flags.join(" ")
230
+ end
231
+ ex("#{rsync} -e '#{ssh_command(port)}' #{flags} #{target}:#{source} #{dest}")
232
+ end
233
+
234
+ # Runs the command on the given host via ssh call
235
+ #
236
+ # @param target [String] ssh host to run command on (user@machine)
237
+ # @param command [String] command to run on the target
238
+ # @param port [Integer] port number for ssh (default 22)
239
+ # @param return_command_output [Boolean] whether or not command output should be returned
240
+ # @return [true, String] Returns true if the command was successful or the
241
+ # output of the command if return_command_output is true
242
+ # @raise [RuntimeError] If there is no target given or the command fails an exception is raised
243
+ def remote_ssh_command(target, command, port = 22, return_command_output: false)
244
+ if target
245
+ puts "Executing '#{command}' on #{target}"
246
+ if return_command_output
247
+ ret = %x(#{ssh_command(port)} -T #{target} '#{command.gsub("'", "'\\\\''")}').chomp
248
+ if $?.success?
249
+ return ret
250
+ else
251
+ raise "Remote ssh command (#{command}) failed on '#{target}'."
252
+ end
253
+ else
254
+ Kernel.system("#{ssh_command(port)} -T #{target} '#{command.gsub("'", "'\\\\''")}'")
255
+ $?.success? or raise "Remote ssh command (#{command}) failed on '#{target}'."
256
+ end
257
+ else
258
+ fail "Need a target to ssh to. Received none."
259
+ end
260
+ end
261
+
262
+ # Runs the command on the local host
263
+ #
264
+ # @param command [String] command to run on the target
265
+ # @return [true] Returns true if the command was successful
266
+ # @raise [RuntimeError] If the command fails an exception is raised
267
+ def local_command(command, workdir)
268
+ puts "Executing '#{command}' locally in #{workdir}"
269
+ Kernel.system(command, :chdir => workdir)
270
+ $?.success? or raise "Local command (#{command}) failed."
271
+ end
272
+
273
+ # Helper method that takes a template file and runs it through ERB
274
+ #
275
+ # @param erbfile [String] template to be evaluated
276
+ # @param b [Binding] binding to evaluate the template under
277
+ # @return [String] the evaluated template
278
+ def erb_string(erbfile, b = binding)
279
+ template = File.read(erbfile)
280
+ message = ERB.new(template, nil, "-")
281
+ message.result(b)
282
+ end
283
+
284
+ # Helper method that takes a template and writes the evaluated contents to a file on disk
285
+ #
286
+ # @param erbfile [String]
287
+ # @param outfile [String]
288
+ # @param remove_orig [true, false]
289
+ # @param opts [Hash]
290
+ def erb_file(erbfile, outfile = nil, remove_orig = false, opts = { :binding => binding })
291
+ outfile ||= File.join(Dir.mktmpdir, File.basename(erbfile).sub(File.extname(erbfile), ""))
292
+ output = erb_string(erbfile, opts[:binding])
293
+ File.open(outfile, 'w') { |f| f.write output }
294
+ puts "Generated: #{outfile}"
295
+ FileUtils.rm_rf erbfile if remove_orig
296
+ outfile
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,3 @@
1
+ {
2
+ "thing": "stuff"
3
+ }
@@ -0,0 +1 @@
1
+ /opt/puppetlabs/puppet/bin/ruby -s mcollective -u root -a '/opt/puppetlabs/puppet/bin/mcollectived --config=/etc/puppetlabs/mcollective/server.cfg '
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "git@github.com:puppetlabs/puppet",
3
+ "ref": "3.7.3"
4
+ }
@@ -0,0 +1,103 @@
1
+ require 'vanagon/common/pathname'
2
+
3
+ describe "Vanagon::Common::Pathname" do
4
+ describe "#has_overrides?" do
5
+ it "is false for a pathname with just a path" do
6
+ dir = Vanagon::Common::Pathname.new("/a/b/c")
7
+ expect(dir.has_overrides?).to be(false)
8
+ end
9
+
10
+ it "is true if the pathname has more than a path set" do
11
+ dir = Vanagon::Common::Pathname.new("/a/b/c", mode: '0755')
12
+ expect(dir.has_overrides?).to be(true)
13
+ end
14
+ end
15
+
16
+ describe "#file" do
17
+ it 'creates a new Pathname instance, marked as a file' do
18
+ dir = Vanagon::Common::Pathname.file("/a/b/c/")
19
+ expect(dir.class).to eq(Vanagon::Common::Pathname)
20
+ expect(dir.configfile?).to eq(false)
21
+ end
22
+ end
23
+
24
+ describe "#configfile" do
25
+ it 'creates a new Pathname instance, marked as a configuration file' do
26
+ dir = Vanagon::Common::Pathname.configfile("/a/b/c/")
27
+ expect(dir.class).to eq(Vanagon::Common::Pathname)
28
+ expect(dir.configfile?).to eq(true)
29
+ end
30
+ end
31
+
32
+ describe "#initialize" do
33
+ it 'strips trailing slashes off of the path to normalize it' do
34
+ dir = Vanagon::Common::Pathname.new("/a/b/c/")
35
+ expect(dir.path).to eq("/a/b/c")
36
+ end
37
+
38
+ it 'removes extra / from the pathname to normalize it' do
39
+ dir = Vanagon::Common::Pathname.new("/a//b///c///")
40
+ expect(dir.path).to eq("/a/b/c")
41
+ end
42
+ end
43
+
44
+ describe "equality" do
45
+ it "is not equal if the paths differ" do
46
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c")
47
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c/d")
48
+ expect(dir1).not_to eq(dir2)
49
+ end
50
+
51
+ it "is not equal if there are different attributes set" do
52
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c")
53
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
54
+ expect(dir1).not_to eq(dir2)
55
+ end
56
+
57
+ it "is equal if there are the same attributes set to the same values" do
58
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
59
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
60
+ expect(dir1).to eq(dir2)
61
+ end
62
+
63
+ it "is equal if the paths are the same and the only attribute set" do
64
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c")
65
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c")
66
+ expect(dir1).to eq(dir2)
67
+ end
68
+ end
69
+
70
+ describe "#hash" do
71
+ it "has the same hash is the attributes are the same" do
72
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
73
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
74
+ expect(dir1.hash).to eq(dir2.hash)
75
+ end
76
+
77
+ it "has different hashes if any attribute is different" do
78
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123', owner: 'alice')
79
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123', owner: 'bob')
80
+ expect(dir1.hash).to_not eq(dir2.hash)
81
+ end
82
+ end
83
+
84
+ describe "uniqueness of pathnames" do
85
+ it "should only add 1 Pathname object with the same attributes to a set" do
86
+ set = Set.new
87
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
88
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
89
+ dir3 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123', owner: 'alice')
90
+ set << dir1 << dir2 << dir3
91
+ expect(set.size).to eq(2)
92
+ end
93
+
94
+ it "should reduce an array to unique elements successfully" do
95
+ dir1 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
96
+ dir2 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123')
97
+ dir3 = Vanagon::Common::Pathname.new("/a/b/c", mode: '0123', owner: 'alice')
98
+ arr = [ dir1, dir2, dir3 ]
99
+ expect(arr.size).to eq(3)
100
+ expect(arr.uniq.size).to eq(2)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,36 @@
1
+ require 'vanagon/common/user'
2
+
3
+ describe 'Vanagon::Common::User' do
4
+ describe 'initialize' do
5
+ it 'group defaults to the name of the user' do
6
+ user = Vanagon::Common::User.new('willamette')
7
+ expect(user.group).to eq('willamette')
8
+ end
9
+ end
10
+
11
+ describe 'equality' do
12
+ it 'is not equal if the names differ' do
13
+ user1 = Vanagon::Common::User.new('willamette')
14
+ user2 = Vanagon::Common::User.new('columbia')
15
+ expect(user1).not_to eq(user2)
16
+ end
17
+
18
+ it 'is not equal if there are different attributes set' do
19
+ user1 = Vanagon::Common::User.new('willamette', 'group1')
20
+ user2 = Vanagon::Common::User.new('willamette', 'group2')
21
+ expect(user1).not_to eq(user2)
22
+ end
23
+
24
+ it 'is equal if there are the same attributes set to the same values' do
25
+ user1 = Vanagon::Common::User.new('willamette', 'group')
26
+ user2 = Vanagon::Common::User.new('willamette', 'group')
27
+ expect(user1).to eq(user2)
28
+ end
29
+
30
+ it 'is equal if the name are the same and the only attribute set' do
31
+ user1 = Vanagon::Common::User.new('willamette')
32
+ user2 = Vanagon::Common::User.new('willamette')
33
+ expect(user1).to eq(user2)
34
+ end
35
+ end
36
+ end