pointer 0.0.1
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/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +25 -0
- data/Rakefile +1 -0
- data/TODO.md +34 -0
- data/bin/pointer +58 -0
- data/config/pointer.rb.default +60 -0
- data/lib/pointer.rb +74 -0
- data/lib/pointer/bit_bucket.rb +25 -0
- data/lib/pointer/deployer_application.rb +52 -0
- data/lib/pointer/helpers.rb +23 -0
- data/lib/pointer/mina.rb +89 -0
- data/lib/pointer/nginx_passenger.rb +63 -0
- data/lib/pointer/postgres.rb +43 -0
- data/lib/pointer/rails_user.rb +48 -0
- data/lib/pointer/rvm.rb +25 -0
- data/lib/pointer/ssh_helpers.rb +84 -0
- data/lib/pointer/variables.rb +59 -0
- data/lib/pointer/version.rb +3 -0
- data/pointer.gemspec +28 -0
- metadata +170 -0
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
/.idea
|
19
|
+
|
20
|
+
# Do not commit this file, it has root password
|
21
|
+
/config/pointer.rb
|
22
|
+
|
23
|
+
/test_repo
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Slava Vishnyakov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Pointer
|
2
|
+
|
3
|
+
Super-simple deployment! (Yes, I'll write a README)
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'pointer'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install pointer
|
18
|
+
|
19
|
+
## Contributing
|
20
|
+
|
21
|
+
1. Fork it
|
22
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
23
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
24
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
25
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/TODO.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
## Issues
|
2
|
+
|
3
|
+
vagrant for tests
|
4
|
+
touch: cannot touch `tmp/restart.txt': No such file or directory (via deploy)
|
5
|
+
|
6
|
+
automate bitbicket deployment key installation
|
7
|
+
что если nginx уже поставлен?
|
8
|
+
support apache
|
9
|
+
что если apache уже поставлен?
|
10
|
+
support capistrano
|
11
|
+
check that mina installed
|
12
|
+
ufw
|
13
|
+
fail2ban
|
14
|
+
postgres
|
15
|
+
mysql
|
16
|
+
mongo ?
|
17
|
+
revoke sudo nopasswd
|
18
|
+
webhook acceptor - min processes
|
19
|
+
Assumes 'remote.origin'
|
20
|
+
Fix locales (/etc/environment) on Ubuntu
|
21
|
+
что может пойти не так с публично доступным deploy? add md5=...
|
22
|
+
remove all https://raw.github.com/slava-vishnyakov/useful-stuff/master/init.d-nginx.conf
|
23
|
+
detect bitbucket keys https://bitbucket.org/username/test2/admin/deploy-keys
|
24
|
+
|
25
|
+
once installed postgres will not allow second creation
|
26
|
+
local git hosting on server? bad idea, but really easy
|
27
|
+
|
28
|
+
@ssh.exec! -> expect_success
|
29
|
+
session identifier shared
|
30
|
+
"your website is at"
|
31
|
+
|
32
|
+
check for gem pg in Gemfile
|
33
|
+
|
34
|
+
backups?
|
data/bin/pointer
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pointer'
|
4
|
+
|
5
|
+
config_file = 'config/pointer.rb'
|
6
|
+
|
7
|
+
@options = {}
|
8
|
+
def set(name, value)
|
9
|
+
@options[name] = value
|
10
|
+
end
|
11
|
+
|
12
|
+
if ARGV[0] == 'init'
|
13
|
+
FileUtils.mkdir('config') unless File.exists?('config')
|
14
|
+
|
15
|
+
if File.exists? (config_file)
|
16
|
+
puts "File '#{config_file}' already exists"
|
17
|
+
exit
|
18
|
+
else
|
19
|
+
FileUtils.cp(File.expand_path(File.dirname(__FILE__)) + '/../config/pointer.rb.default', config_file)
|
20
|
+
puts "Please edit '#{config_file}'"
|
21
|
+
end
|
22
|
+
|
23
|
+
unless File.exists?('.git')
|
24
|
+
puts "Please run it from Git repository"
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
unless File.exists?('config.ru')
|
29
|
+
puts "'config.ru' file is required"
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
gitignore = File.exists?('.gitignore') ? IO.read('.gitignore') : ''
|
34
|
+
|
35
|
+
unless gitignore.include? '/' + config_file
|
36
|
+
gitignore = gitignore.rstrip + "\n\n# Do not commit this file, it has root password\n" + '/' + config_file
|
37
|
+
IO.write('.gitignore', gitignore)
|
38
|
+
end
|
39
|
+
|
40
|
+
elsif ARGV[0] == 'deploy'
|
41
|
+
require './' + config_file
|
42
|
+
Pointer::EasyDeploy.new.run! @options
|
43
|
+
|
44
|
+
elsif ARGV[0] == 'ssh'
|
45
|
+
require './' + config_file
|
46
|
+
system("ssh -i #{@options[:private_key]} -p '#{@options[:ssh_port]}' #{@options[:rails_user]}@#{@options[:ssh_host]}")
|
47
|
+
|
48
|
+
elsif ARGV[0] == 'ssh:log'
|
49
|
+
require './' + config_file
|
50
|
+
system("ssh -i #{@options[:private_key]} -p '#{@options[:ssh_port]}' #{@options[:rails_user]}@#{@options[:ssh_host]} \"tail --lines=1000 #{@options[:site_dir]}/current/log/production.log\" | less")
|
51
|
+
|
52
|
+
else
|
53
|
+
puts "You can run those:"
|
54
|
+
puts " pointer init"
|
55
|
+
puts " pointer deploy"
|
56
|
+
puts " pointer ssh"
|
57
|
+
puts " pointer ssh:log"
|
58
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# What is your website name?
|
2
|
+
|
3
|
+
host = 'website.com'
|
4
|
+
set :host, host
|
5
|
+
|
6
|
+
# Where do we deploy this project?
|
7
|
+
# Usually same as above, so we use the same variable
|
8
|
+
|
9
|
+
set :ssh_host, host
|
10
|
+
|
11
|
+
# Yes, we need super-user for now, in future - maybe sudoer will suffice
|
12
|
+
# Don't worry, this file (config/pointer.rb) will not be commited to your Git repo
|
13
|
+
# this file is needed only once to config the server, so your root password
|
14
|
+
# will not be compromised
|
15
|
+
|
16
|
+
set :ssh_user, 'root'
|
17
|
+
set :ssh_port, 22
|
18
|
+
set :password, 'password_of_root'
|
19
|
+
|
20
|
+
# Your public and private key that will be used for deployment
|
21
|
+
|
22
|
+
set :public_key, "~/.ssh/id_rsa.pub"
|
23
|
+
set :private_key, "~/.ssh/id_rsa"
|
24
|
+
|
25
|
+
# Less privileged user that you will use to deploy the project
|
26
|
+
|
27
|
+
rails_user = 'rails'
|
28
|
+
set :rails_user, rails_user
|
29
|
+
|
30
|
+
# Configuration of your website, by default - it's same as ssh_host
|
31
|
+
|
32
|
+
set :site_host, host
|
33
|
+
set :site_port, 80
|
34
|
+
set :site_dir, "/home/#{rails_user}/applications/#{host}"
|
35
|
+
|
36
|
+
# Do we plan to host your site on BitBucket?
|
37
|
+
# BitBucket is the only reputable repo hosting that I know that allows free private repositories
|
38
|
+
|
39
|
+
set :git_repo, 'git@bitbucket.org:repo/repo.git'
|
40
|
+
|
41
|
+
# Currently only NginxPassenger is supported
|
42
|
+
# in future :apache will be supported too
|
43
|
+
|
44
|
+
set :web_server, :nginx
|
45
|
+
|
46
|
+
# For now we only support mina,
|
47
|
+
# in future we will support :capistrano
|
48
|
+
|
49
|
+
set :deployer, :mina
|
50
|
+
|
51
|
+
# Do you need to setup automatic updates using WebHook,
|
52
|
+
# so that when you push the repo - it gets automatically deployed
|
53
|
+
|
54
|
+
set :deployer_application, true
|
55
|
+
|
56
|
+
# Do we need to install and create a database for you?
|
57
|
+
# For now only :postgres is supported
|
58
|
+
# The database will be created and attached via database.yml
|
59
|
+
|
60
|
+
set :database, :postgres
|
data/lib/pointer.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require "pointer/version"
|
2
|
+
require 'active_support/secure_random'
|
3
|
+
|
4
|
+
Dir[File.expand_path(File.dirname(__FILE__) + '/pointer/**.rb')].each do |file|
|
5
|
+
require file
|
6
|
+
end
|
7
|
+
|
8
|
+
module Pointer
|
9
|
+
class EasyDeploy
|
10
|
+
include Postgres
|
11
|
+
include Helpers
|
12
|
+
include Variables
|
13
|
+
include SshHelpers
|
14
|
+
include BitBucket
|
15
|
+
include DeployerApplication
|
16
|
+
include Mina
|
17
|
+
include NginxPassenger
|
18
|
+
include RailsUser
|
19
|
+
include Rvm
|
20
|
+
|
21
|
+
def run!(options)
|
22
|
+
@options = options
|
23
|
+
|
24
|
+
assert_git()
|
25
|
+
check_mina_prereqs()
|
26
|
+
|
27
|
+
with_root_ssh do
|
28
|
+
# delete_rails_user()
|
29
|
+
create_rails_user()
|
30
|
+
upload_public_key()
|
31
|
+
add_sudo()
|
32
|
+
end
|
33
|
+
|
34
|
+
with_ssh do
|
35
|
+
test_connection()
|
36
|
+
|
37
|
+
install_rvm()
|
38
|
+
|
39
|
+
if nginx
|
40
|
+
install_passenger()
|
41
|
+
create_site_config()
|
42
|
+
end
|
43
|
+
|
44
|
+
if mina
|
45
|
+
mina_init()
|
46
|
+
end
|
47
|
+
|
48
|
+
if postgres
|
49
|
+
@db_config = install_postgres()
|
50
|
+
end
|
51
|
+
|
52
|
+
print_deploy_key()
|
53
|
+
|
54
|
+
if bitbucket
|
55
|
+
add_bit_bucket_host()
|
56
|
+
end
|
57
|
+
|
58
|
+
if mina
|
59
|
+
mina_deploy()
|
60
|
+
end
|
61
|
+
|
62
|
+
if deployer_application
|
63
|
+
install_deployer()
|
64
|
+
end
|
65
|
+
|
66
|
+
revoke_sudo()
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Pointer
|
2
|
+
module BitBucket
|
3
|
+
def add_bit_bucket_host()
|
4
|
+
what "Use this as deploy key"
|
5
|
+
puts '----'
|
6
|
+
puts get_file_contents('/home/rails/.ssh/id_rsa.pub')
|
7
|
+
puts '----'
|
8
|
+
|
9
|
+
if bitbucket
|
10
|
+
if @ssh.exec!('bash -lc "ssh -o StrictHostKeyChecking=no git@bitbucket.org"') =~ /Permission denied/
|
11
|
+
puts "Press ENTER when you added this deploy key to repository".red
|
12
|
+
STDIN.readline()
|
13
|
+
end
|
14
|
+
else
|
15
|
+
puts "You need to ssh into your machine as 'ssh #{rails_user}@#{host} -p #{port}'"
|
16
|
+
puts "then connect via ssh to your repository hosting, like so 'ssh git@github.com' and accept the key"
|
17
|
+
puts "Then you need to install the deploy key above"
|
18
|
+
puts "Press ENTER when you added this deploy key to repository".red
|
19
|
+
STDIN.readline()
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Pointer
|
2
|
+
module DeployerApplication
|
3
|
+
def install_deployer()
|
4
|
+
deployer_port = site_port.to_i + 8000
|
5
|
+
# TODO: detect if mina and sinatra already installed
|
6
|
+
puts rvm! "gem install mina"
|
7
|
+
puts rvm! "gem install sinatra"
|
8
|
+
|
9
|
+
local_key = get_file_contents('/home/rails/.ssh/id_rsa.pub')
|
10
|
+
ensure_file_contains("/home/rails/.ssh/authorized_keys", local_key)
|
11
|
+
puts @ssh.exec! "ssh -o StrictHostKeyChecking=no rails@#{host} pwd"
|
12
|
+
|
13
|
+
puts @ssh.exec! "mkdir -p applications/pointer/#{host}-#{site_port}/public"
|
14
|
+
|
15
|
+
|
16
|
+
server = "
|
17
|
+
server {
|
18
|
+
listen #{deployer_port};
|
19
|
+
server_name #{host};
|
20
|
+
passenger_enabled on;
|
21
|
+
passenger_user rails;
|
22
|
+
passenger_max_requests 10;
|
23
|
+
root /home/rails/applications/pointer/#{host}-#{site_port}/public;
|
24
|
+
}
|
25
|
+
"
|
26
|
+
|
27
|
+
put_file_contents("/opt/nginx/conf/rails-sites/pointer-#{host}-#{deployer_port}.conf", server)
|
28
|
+
|
29
|
+
mtime = "#{host}-#{site_port}"
|
30
|
+
config_ru = "
|
31
|
+
require 'sinatra'
|
32
|
+
|
33
|
+
post '/deploy/#{mtime}' do
|
34
|
+
`cd / && mina -f /home/rails/applications/pointer/#{host}-#{site_port}/deploy.rb deploy` # < /dev/null > /dev/null && echo OK
|
35
|
+
end
|
36
|
+
|
37
|
+
run Sinatra::Application
|
38
|
+
"
|
39
|
+
|
40
|
+
put_file_contents("applications/pointer/#{host}-#{site_port}/deploy.rb", IO.read('./config/deploy.rb'))
|
41
|
+
put_file_contents("applications/pointer/#{host}-#{site_port}/config.ru", config_ru)
|
42
|
+
puts @ssh.exec!('sudo service nginx restart')
|
43
|
+
|
44
|
+
hook_url = "http://#{host}:#{deployer_port}/deploy/#{mtime}"
|
45
|
+
puts "WebHook (POST) address: %s" % [hook_url.green]
|
46
|
+
puts "Add this to GitHub/BitBucket as WebHook/POST service, so that your code is automatically deployed on every push"
|
47
|
+
puts "Run, for example: curl -X POST #{hook_url}"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Pointer
|
4
|
+
module Helpers
|
5
|
+
def remote_repo()
|
6
|
+
`git config --get remote.origin.url`.gsub(/^ssh:\/\/(.*?)\//, '\\1:').strip
|
7
|
+
end
|
8
|
+
|
9
|
+
def assert_git()
|
10
|
+
unless File.exists?('.git')
|
11
|
+
raise "Please run from a Git repository (use private Git hosting from bitbucket.org for example)"
|
12
|
+
end
|
13
|
+
|
14
|
+
if remote_repo.to_s == ''
|
15
|
+
raise "This tool assumes you have git remote branch, named #{'origin'.green}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def what(string)
|
20
|
+
puts "# #{string}".green
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/pointer/mina.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
module Pointer
|
2
|
+
module Mina
|
3
|
+
def check_mina_prereqs
|
4
|
+
if File.exists?('config/deploy.rb') and not ARGV.include?('--unlink-mina')
|
5
|
+
user_has_mina_config()
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def mina_deploy()
|
10
|
+
what "[local -> remote] mina setup"
|
11
|
+
|
12
|
+
mina_setup = `mina setup </dev/null || echo "[ERROR]"`
|
13
|
+
puts mina_setup
|
14
|
+
if mina_setup.include? '[ERROR]'
|
15
|
+
raise "mina setup failed"
|
16
|
+
end
|
17
|
+
|
18
|
+
remote_db_config_file = "#{@options[:site_dir]}/shared/config/database.yml"
|
19
|
+
|
20
|
+
if @db_config
|
21
|
+
puts "I have a database config, putting it there"
|
22
|
+
put_file_contents(remote_db_config_file, @db_config)
|
23
|
+
else
|
24
|
+
if file_absent(remote_db_config_file)
|
25
|
+
db_config = IO.read(@options[:site_dir] + "/shared/config/database.yml")
|
26
|
+
put_file_contents(remote_db_config_file, db_config)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
what "[local -> remote] mina deploy"
|
31
|
+
|
32
|
+
mina_deploy = `mina deploy </dev/null || echo "[ERROR]"`
|
33
|
+
puts mina_deploy
|
34
|
+
if mina_deploy.include? '[ERROR]'
|
35
|
+
raise "mina deploy failed"
|
36
|
+
end
|
37
|
+
|
38
|
+
what "done"
|
39
|
+
end
|
40
|
+
|
41
|
+
def mina_init()
|
42
|
+
if ARGV.include? '--unlink-mina'
|
43
|
+
puts "Unlinking mina config".on_red
|
44
|
+
File.unlink('config/deploy.rb') if File.exists? 'config/deploy.rb'
|
45
|
+
end
|
46
|
+
|
47
|
+
rvm_version = rvm!('rvm-prompt').strip
|
48
|
+
|
49
|
+
#`gem install mina`
|
50
|
+
what "[local] mina init"
|
51
|
+
mina_init = `mina init </dev/null || echo "[ERROR]"`
|
52
|
+
if mina_init.include? '[ERROR]'
|
53
|
+
puts mina_init
|
54
|
+
user_has_mina_config()
|
55
|
+
end
|
56
|
+
|
57
|
+
config = IO.read('config/deploy.rb')
|
58
|
+
config = config.gsub("# require 'mina/rvm'", "require 'mina/rvm'")
|
59
|
+
config = config.gsub("set :domain, 'foobar.com'", "set :domain, #{@options[:site_host].inspect}")
|
60
|
+
config = config.gsub("set :deploy_to, '/var/www/foobar.com'", "set :deploy_to, #{@options[:site_dir].inspect}")
|
61
|
+
config = config.gsub("# set :user, 'foobar'", " set :user, '#{rails_user}'")
|
62
|
+
config = config.gsub("# invoke :'rvm:use[ruby-1.9.3-p125@default]'", "invoke :'rvm:use[#{rvm_version}]'")
|
63
|
+
config = config.gsub("queue %[-----> Be sure to edit 'shared/config/database.yml'.]", "")
|
64
|
+
puts "Be sure to edit 'shared/config/database.yml'".on_red
|
65
|
+
config = config.gsub("set :repository, 'git://...'", "set :repository, #{remote_repo.inspect}",)
|
66
|
+
#config = config.gsub("set :shared_paths, ['config/database.yml', 'log']", "set :shared_paths, ['log', 'sqlite']",)
|
67
|
+
|
68
|
+
#database_symlink = "\n" +
|
69
|
+
# ' queue %[rm "#{deploy_to}/current/config/database.yml"]' + "\n" +
|
70
|
+
# ' queue %[ln -s "#{deploy_to}/shared/config/database.yml" "#{deploy_to}/current/config/database.yml"]' + "\n" +
|
71
|
+
# #' queue %[cat "#{deploy_to}/shared/config/database.yml"]' + "\n" +
|
72
|
+
# "\n "
|
73
|
+
#
|
74
|
+
#config = config.gsub("queue 'touch tmp/restart.txt'", database_symlink + "queue 'touch tmp/restart.txt'")
|
75
|
+
config = config.gsub("queue 'touch tmp/restart.txt'", 'queue "mkdir tmp; touch tmp/restart.txt"')
|
76
|
+
IO.write('config/deploy.rb', config)
|
77
|
+
# puts "Run #{'mina setup'.red} and #{'mina deploy'.red}"
|
78
|
+
|
79
|
+
`ssh -o StrictHostKeyChecking=no #{rails_user}@#{host} pwd`
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def user_has_mina_config
|
84
|
+
puts "Run %s if you already have everything set up" % ["mina deploy".green]
|
85
|
+
puts "or run with --unlink-mina if you want to force mina config generation"
|
86
|
+
exit
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Pointer
|
2
|
+
module NginxPassenger
|
3
|
+
def install_passenger()
|
4
|
+
unless file_exists('/opt/nginx')
|
5
|
+
what 'Install nginx/passenger'
|
6
|
+
rvm!('gem install passenger')
|
7
|
+
@ssh.exec!('sudo mkdir /opt')
|
8
|
+
@ssh.exec!('sudo mkdir /opt/nginx')
|
9
|
+
@ssh.exec!('sudo chown rails:rails /opt/nginx')
|
10
|
+
rvm!('passenger-install-nginx-module --auto --auto-download --prefix=/opt/nginx')
|
11
|
+
|
12
|
+
what "Installing nginx init.d"
|
13
|
+
@ssh.exec!('sudo wget https://raw.github.com/slava-vishnyakov/useful-stuff/master/init.d-nginx.conf -O /etc/init.d/nginx')
|
14
|
+
|
15
|
+
what "Changing nginx.conf permissions"
|
16
|
+
@ssh.exec!('sudo chmod o+x /etc/init.d/nginx')
|
17
|
+
|
18
|
+
what "update-rc.d"
|
19
|
+
@ssh.exec!('sudo update-rc.d nginx defaults')
|
20
|
+
|
21
|
+
what "Installing nginx.conf"
|
22
|
+
@ssh.exec!('mv /opt/nginx/conf/nginx.conf /opt/nginx/conf/nginx.conf-orig')
|
23
|
+
@ssh.exec!('wget https://raw.github.com/slava-vishnyakov/useful-stuff/master/nginx.conf -O /opt/nginx/conf/nginx.conf')
|
24
|
+
|
25
|
+
what "Replacing passenger_ruby and passenger_root with actual Passenger data"
|
26
|
+
orig_file = get_file_contents '/opt/nginx/conf/nginx.conf-orig'
|
27
|
+
new_file = get_file_contents '/opt/nginx/conf/nginx.conf'
|
28
|
+
new_file.sub! /passenger_ruby (.*?);/, orig_file.match(/passenger_ruby (.*?);/)[0]
|
29
|
+
new_file.sub! /passenger_root (.*?);/, orig_file.match(/passenger_root (.*?);/)[0]
|
30
|
+
put_file_contents '/opt/nginx/conf/nginx.conf', new_file
|
31
|
+
|
32
|
+
what "Creating /opt/nginx/conf/rails-sites"
|
33
|
+
@ssh.exec!('mkdir /opt/nginx/conf/rails-sites')
|
34
|
+
|
35
|
+
what "Starting nginx"
|
36
|
+
@ssh.exec!('sudo service nginx start')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_site_config
|
41
|
+
config = "
|
42
|
+
server {
|
43
|
+
listen #{@options[:site_port]};
|
44
|
+
server_name #{@options[:site_host]};
|
45
|
+
passenger_enabled on;
|
46
|
+
root #{@options[:site_dir]}/current/public;
|
47
|
+
passenger_user rails;
|
48
|
+
passenger_max_requests 500;
|
49
|
+
}
|
50
|
+
"
|
51
|
+
config_file = "/opt/nginx/conf/rails-sites/#{@options[:site_host]}-#{@options[:site_port]}.conf"
|
52
|
+
|
53
|
+
if file_absent(config_file)
|
54
|
+
put_file_contents(config_file, config)
|
55
|
+
end
|
56
|
+
|
57
|
+
if @ssh.exec!("sudo /opt/nginx/sbin/nginx -t") =~ /test is successful/
|
58
|
+
@ssh.exec!("sudo service nginx reload")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Pointer
|
2
|
+
module Postgres
|
3
|
+
def install_postgres
|
4
|
+
# install_postgres_database
|
5
|
+
unless @ssh.exec! 'which psql'
|
6
|
+
# official apt repo does not support 12.10 (quantal) yet :(
|
7
|
+
puts @ssh.exec! 'sudo apt-get install -y libpq-dev'
|
8
|
+
puts @ssh.exec! 'sudo apt-get install -y software-properties-common'
|
9
|
+
puts @ssh.exec! 'sudo add-apt-repository ppa:pitti/postgresql'
|
10
|
+
puts @ssh.exec! 'sudo apt-get update'
|
11
|
+
puts @ssh.exec! 'sudo apt-get install -y postgresql-9.2'
|
12
|
+
end
|
13
|
+
|
14
|
+
# create_database
|
15
|
+
|
16
|
+
if file_absent("#{@options[:site_dir]}/shared/config/database.yml")
|
17
|
+
o = [('a'..'z'), ('A'..'Z'), ('0'..'9')].map { |i| i.to_a }.flatten
|
18
|
+
|
19
|
+
# TODO: save these on server??
|
20
|
+
password = (0...16).map { o[rand(o.length)] }.join
|
21
|
+
username = 'user_' + host.gsub(/[^a-z0-9]/, '_')
|
22
|
+
database = 'db_' + host.gsub(/[^a-z0-9]/, '_')
|
23
|
+
|
24
|
+
# sudo sudo -u postgres - because we only have password-less sudo to root
|
25
|
+
puts @ssh.exec!("sudo sudo -u postgres psql -c \"CREATE ROLE #{username} WITH CREATEDB LOGIN PASSWORD '#{password}'\"")
|
26
|
+
puts @ssh.exec!("sudo sudo -u postgres psql -c \"CREATE DATABASE #{database} OWNER #{username}\"");
|
27
|
+
|
28
|
+
# create database.yml
|
29
|
+
"production:\n" +
|
30
|
+
" adapter: postgresql\n" +
|
31
|
+
" host: 127.0.0.1\n" +
|
32
|
+
" encoding: utf8\n" +
|
33
|
+
" database: #{database}\n" +
|
34
|
+
" username: #{username}\n" +
|
35
|
+
" password: #{password}\n" +
|
36
|
+
''
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Pointer
|
2
|
+
module RailsUser
|
3
|
+
def delete_rails_user
|
4
|
+
puts @ssh.exec!("userdel rails -f -r")
|
5
|
+
end
|
6
|
+
|
7
|
+
def create_rails_user
|
8
|
+
if @ssh.exec!("id rails") =~ /No such user/
|
9
|
+
expect_empty @ssh.exec!("useradd rails -d /home/#{rails_user} -m -s /bin/bash")
|
10
|
+
expect_empty @ssh.exec!("usermod -a -G sudo rails")
|
11
|
+
what "User created: rails"
|
12
|
+
else
|
13
|
+
what "User exists: rails"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def upload_public_key
|
18
|
+
ssh_dir = "/home/#{rails_user}/.ssh"
|
19
|
+
authorized_keys_file = ssh_dir + '/authorized_keys'
|
20
|
+
|
21
|
+
what "Uploading private key"
|
22
|
+
@ssh.exec!("mkdir #{ssh_dir}")
|
23
|
+
ensure_file_contains(authorized_keys_file, IO.read(File.expand_path(public_key)))
|
24
|
+
expect_empty @ssh.exec!("chown rails:rails #{ssh_dir}")
|
25
|
+
expect_empty @ssh.exec!("chown rails:rails #{authorized_keys_file}")
|
26
|
+
expect_empty @ssh.exec!("chmod 0700 #{ssh_dir}")
|
27
|
+
expect_empty @ssh.exec!("chmod 0600 #{authorized_keys_file}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_sudo
|
31
|
+
sudo_string = "rails ALL = NOPASSWD:ALL"
|
32
|
+
ensure_file_contains('/etc/sudoers', sudo_string)
|
33
|
+
end
|
34
|
+
|
35
|
+
def revoke_sudo
|
36
|
+
puts "revoke_sudo is not implemented"
|
37
|
+
end
|
38
|
+
|
39
|
+
def print_deploy_key
|
40
|
+
id_rsa = '/home/rails/.ssh/id_rsa'
|
41
|
+
if file_absent(id_rsa)
|
42
|
+
what "Generating ssh key"
|
43
|
+
puts @ssh.exec!("ssh-keygen -q -t rsa -f #{id_rsa} -N ''")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/pointer/rvm.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Pointer
|
2
|
+
module Rvm
|
3
|
+
def install_rvm
|
4
|
+
if file_absent("/home/#{rails_user}/.rvm/scripts/rvm")
|
5
|
+
what "Update system"
|
6
|
+
puts @ssh.exec!("sudo apt-get -qq -y update")
|
7
|
+
what "Install build stuff"
|
8
|
+
puts @ssh.exec!("sudo apt-get -qq -y install curl libcurl4-gnutls-dev git nodejs build-essential openssl " +
|
9
|
+
" libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev " +
|
10
|
+
" libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf " +
|
11
|
+
" libc6-dev ncurses-dev automake libtool bison subversion pkg-config libgdbm-dev libffi-dev"
|
12
|
+
)
|
13
|
+
what "Actually install RVM (this takes some time)"
|
14
|
+
puts @ssh.exec!("\\curl -L https://get.rvm.io | bash -s stable --ruby=#{ruby_version}")
|
15
|
+
|
16
|
+
rvm!("rvm use #{ruby_version} --default")
|
17
|
+
ensure_file_contains('/home/rails/.gemrc', 'gem: --no-ri --no-rdoc')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def rvm!(command)
|
22
|
+
@ssh.exec!(". /home/#{rails_user}/.rvm/scripts/rvm && (#{command})")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'net/ssh'
|
3
|
+
require 'net/scp'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module Pointer
|
7
|
+
module SshHelpers
|
8
|
+
def get_file_contents(file_name)
|
9
|
+
@ssh.exec!("cat #{file_name.shellescape}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def put_file_contents(file_name, string)
|
13
|
+
file = Tempfile.new('ssh')
|
14
|
+
file.write(string)
|
15
|
+
file.rewind
|
16
|
+
@ssh.scp.upload!(file.path, file_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ensure_file_contains(file_name, string)
|
20
|
+
contents = file_absent(file_name) ? '' : get_file_contents(file_name).to_s
|
21
|
+
unless contents.include? string
|
22
|
+
unless contents.end_with? "\n"
|
23
|
+
contents += "\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
contents += string
|
27
|
+
|
28
|
+
put_file_contents(file_name, contents)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_file_not_contains(file_name, string)
|
33
|
+
contents = file_absent(file_name) ? '' : get_file_contents(file_name).to_s
|
34
|
+
|
35
|
+
if contents and contents.include? string
|
36
|
+
contents.gsub!(string, '')
|
37
|
+
put_file_contents(file_name, contents)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def file_absent(file_name)
|
42
|
+
@ssh.exec!("ls #{file_name.shellescape}") =~ /No such file or directory/
|
43
|
+
end
|
44
|
+
|
45
|
+
def file_exists(file_name)
|
46
|
+
not file_absent(file_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def expect_empty(string)
|
50
|
+
if string
|
51
|
+
puts string
|
52
|
+
exit
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_ssh
|
57
|
+
Net::SSH.start(host, rails_user, port: port, :keys => [private_key], :paranoid => false) do |ssh|
|
58
|
+
@ssh = ssh
|
59
|
+
yield
|
60
|
+
@ssh = 'Connection closed'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def with_root_ssh
|
65
|
+
Net::SSH.start(host, user, port: port, :password => password, :paranoid => false) do |ssh|
|
66
|
+
@ssh = ssh
|
67
|
+
yield
|
68
|
+
@ssh = 'Connection closed'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_connection
|
73
|
+
connection_test = `ssh -o StrictHostKeyChecking=no #{rails_user}@#{host} -p #{port} -i #{private_key} "echo OK"`
|
74
|
+
if connection_test.strip == "OK"
|
75
|
+
what "Connected via private key OK"
|
76
|
+
else
|
77
|
+
puts connection_test.on_red
|
78
|
+
raise "I cannot connect via private key!"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Pointer
|
2
|
+
module Variables
|
3
|
+
def rails_user
|
4
|
+
@options[:rails_user]
|
5
|
+
end
|
6
|
+
|
7
|
+
def private_key
|
8
|
+
@options[:private_key]
|
9
|
+
end
|
10
|
+
|
11
|
+
def public_key
|
12
|
+
@options[:public_key]
|
13
|
+
end
|
14
|
+
|
15
|
+
def password
|
16
|
+
@options[:password]
|
17
|
+
end
|
18
|
+
|
19
|
+
def user
|
20
|
+
@options[:ssh_user]
|
21
|
+
end
|
22
|
+
|
23
|
+
def host
|
24
|
+
@options[:ssh_host]
|
25
|
+
end
|
26
|
+
|
27
|
+
def port
|
28
|
+
@options[:ssh_port]
|
29
|
+
end
|
30
|
+
|
31
|
+
def ruby_version
|
32
|
+
@options[:ruby_version] || "1.9.3"
|
33
|
+
end
|
34
|
+
|
35
|
+
def site_port
|
36
|
+
@options[:site_port].to_i || 80
|
37
|
+
end
|
38
|
+
|
39
|
+
def nginx
|
40
|
+
@options[:web_server] == :nginx
|
41
|
+
end
|
42
|
+
|
43
|
+
def bitbucket
|
44
|
+
@options[:git_repo].include? '@bitbucket.org'
|
45
|
+
end
|
46
|
+
|
47
|
+
def mina
|
48
|
+
@options[:deployer] == :mina
|
49
|
+
end
|
50
|
+
|
51
|
+
def deployer_application
|
52
|
+
@options[:deployer_application]
|
53
|
+
end
|
54
|
+
|
55
|
+
def postgres
|
56
|
+
@options[:database] == :postgres
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/pointer.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pointer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pointer"
|
8
|
+
spec.version = Pointer::VERSION
|
9
|
+
spec.authors = ["Slava Vishnyakov"]
|
10
|
+
spec.email = ["bomboze@gmail.com"]
|
11
|
+
spec.description = %q{Quick deploy}
|
12
|
+
spec.summary = %q{Quick deploy}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_dependency "net-ssh"
|
25
|
+
spec.add_dependency "net-scp"
|
26
|
+
spec.add_dependency "colorize"
|
27
|
+
spec.add_dependency "active_support"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pointer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Slava Vishnyakov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-ssh
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: net-scp
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: colorize
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: active_support
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: Quick deploy
|
111
|
+
email:
|
112
|
+
- bomboze@gmail.com
|
113
|
+
executables:
|
114
|
+
- pointer
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- TODO.md
|
124
|
+
- bin/pointer
|
125
|
+
- config/pointer.rb.default
|
126
|
+
- lib/pointer.rb
|
127
|
+
- lib/pointer/bit_bucket.rb
|
128
|
+
- lib/pointer/deployer_application.rb
|
129
|
+
- lib/pointer/helpers.rb
|
130
|
+
- lib/pointer/mina.rb
|
131
|
+
- lib/pointer/nginx_passenger.rb
|
132
|
+
- lib/pointer/postgres.rb
|
133
|
+
- lib/pointer/rails_user.rb
|
134
|
+
- lib/pointer/rvm.rb
|
135
|
+
- lib/pointer/ssh_helpers.rb
|
136
|
+
- lib/pointer/variables.rb
|
137
|
+
- lib/pointer/version.rb
|
138
|
+
- pointer.gemspec
|
139
|
+
homepage: ''
|
140
|
+
licenses:
|
141
|
+
- MIT
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ! '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
segments:
|
153
|
+
- 0
|
154
|
+
hash: -4480820461649365695
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
segments:
|
162
|
+
- 0
|
163
|
+
hash: -4480820461649365695
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 1.8.23
|
167
|
+
signing_key:
|
168
|
+
specification_version: 3
|
169
|
+
summary: Quick deploy
|
170
|
+
test_files: []
|