amoeba_deploy_tools 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +34 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +83 -0
- data/Rakefile +1 -0
- data/amoeba-deploy-tools.gemspec +36 -0
- data/lib/amoeba_deploy_tools/capistrano/common.rb +5 -0
- data/lib/amoeba_deploy_tools/capistrano/recipes.rb +14 -0
- data/lib/amoeba_deploy_tools/capistrano.rb +11 -0
- data/lib/amoeba_deploy_tools/command.rb +105 -0
- data/lib/amoeba_deploy_tools/commands/amoeba.rb +118 -0
- data/lib/amoeba_deploy_tools/commands/app.rb +27 -0
- data/lib/amoeba_deploy_tools/commands/concerns/hooks.rb +45 -0
- data/lib/amoeba_deploy_tools/commands/concerns/ssh.rb +109 -0
- data/lib/amoeba_deploy_tools/commands/key.rb +22 -0
- data/lib/amoeba_deploy_tools/commands/node.rb +119 -0
- data/lib/amoeba_deploy_tools/commands/ssl.rb +45 -0
- data/lib/amoeba_deploy_tools/config.rb +133 -0
- data/lib/amoeba_deploy_tools/data_bag.rb +30 -0
- data/lib/amoeba_deploy_tools/helpers.rb +51 -0
- data/lib/amoeba_deploy_tools/interactive_cocaine_runner.rb +27 -0
- data/lib/amoeba_deploy_tools/logger.rb +67 -0
- data/lib/amoeba_deploy_tools/noisey_cocaine_runner.rb +42 -0
- data/lib/amoeba_deploy_tools/version.rb +3 -0
- data/spec/amoeba_deploy_tools/command_spec.rb +4 -0
- data/spec/amoeba_deploy_tools/commands_spec.rb +91 -0
- data/spec/amoeba_deploy_tools/config_spec.rb +48 -0
- data/spec/amoeba_deploy_tools/helpers_spec.rb +18 -0
- data/spec/spec_helper.rb +21 -0
- metadata +37 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34b12c26a76d623203a1e6ec7b139d49c3296f9f
|
4
|
+
data.tar.gz: 9e27589f78df53ab436c5282a9e4660eca3acf9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8065541a517c22f166f98f752d2f87af8629f2f9722da2b1f885be41a943607f38cb039d5bd7aae35ffb3a8125b37647cf712be5eecdb0443cbe5b2d5e7c7951
|
7
|
+
data.tar.gz: 02b8bed43ff7194127257da289b2b292130e6d2fee4d4636d6e9b35ae75038f1ae4e4d2cf9a58bc4ff4aeb9177d868bc3b3392642e4780b1756bc4209b4faae2
|
data/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
tmp
|
14
|
+
|
15
|
+
# YARD artifacts
|
16
|
+
.yardoc
|
17
|
+
_yardoc
|
18
|
+
doc/
|
19
|
+
|
20
|
+
# Project files
|
21
|
+
.idea
|
22
|
+
|
23
|
+
# For librarian
|
24
|
+
/cookbooks
|
25
|
+
/tmp
|
26
|
+
*.swp
|
27
|
+
|
28
|
+
# RVM stuff we'll ignore for this gem for max flexibility
|
29
|
+
.ruby-version
|
30
|
+
.ruby-gemset
|
31
|
+
.rvmrc
|
32
|
+
|
33
|
+
# Ignore Gemfile.lock in this since it's a gem
|
34
|
+
Gemfile.lock
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Amoeba Consulting
|
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,83 @@
|
|
1
|
+
Amoeba Deploy Tools
|
2
|
+
===================
|
3
|
+
|
4
|
+
Amoeba Deploy Tools (ADT) is a ruby gem that enables rapid creation of servers using the Chef config
|
5
|
+
management system. Using Chef today there are many tools and best practices, and we have found that
|
6
|
+
often setting up a chef "kitchen" can be tedious. Additionally, we believe in supporting server-less
|
7
|
+
deploys. That is, we don't like to maintain and run a separate chef server just to manage our boxes.
|
8
|
+
|
9
|
+
ADT integrates a number of other tools (`chef-solo`, and `librarian`, and others) and we provide a
|
10
|
+
[skeleton Kitchen](http://github.com/AmoebaConsulting/amoeba-kitchen-skel), so you can fork our
|
11
|
+
kitchen, install this gem, and deploy a node in minutes.
|
12
|
+
|
13
|
+
## Short introduction
|
14
|
+
|
15
|
+
First, you need ruby installed. Next, add `amoeba-deploy-tools` to your project Gemfile, or install
|
16
|
+
via the `gem` command:
|
17
|
+
|
18
|
+
> gem install amoeba-deploy-tools
|
19
|
+
|
20
|
+
If you are preparing a server to install an application, change to that project's directory.
|
21
|
+
Otherwise, switch to a dir where you plan on running ADT from. And run:
|
22
|
+
|
23
|
+
> amoeba init
|
24
|
+
|
25
|
+
The command will walk you through the process of initializing your kitchen. Now you've got a
|
26
|
+
kitchen all ready to go. We recommend you add it to version control (git). We do not recommend you
|
27
|
+
add the .amoeba.yml configuration file to git.
|
28
|
+
|
29
|
+
Next you must create a node definition, and run a deploy. A node definition sits in the kitchen's
|
30
|
+
`nodes/` directory. See this directory for a sample node you can copy / rename to make your own.
|
31
|
+
|
32
|
+
Once you have a node defined, just run:
|
33
|
+
|
34
|
+
> amoeba node bootstrap --node <node-name>
|
35
|
+
|
36
|
+
... and the node will be provisioned. After provisioning, the node's metadata will be stored in the
|
37
|
+
`data_bags` folder. Our skeleton kitchen includes a basenode recipe that creates a deployment user
|
38
|
+
on the destination box, and disables root access. This user's name will be cached in the node's
|
39
|
+
databag and used for subsequent operations to the box. Now, as you make changes to the node, or any
|
40
|
+
cookbooks you create in `site-cookbooks`, you can push those changes by running:
|
41
|
+
|
42
|
+
> amoeba node push --node <node-name>
|
43
|
+
|
44
|
+
Finally to setup Capistrano, you need only add the following to your existing Capfile:
|
45
|
+
|
46
|
+
require 'amoeba_deploy_tools/capistrano'
|
47
|
+
|
48
|
+
For a full list of commends, run `amoeba help` or see below.
|
49
|
+
|
50
|
+
## Detailed Information
|
51
|
+
|
52
|
+
In essence, ADT is controlled by a configuration file, `.amoeba.yml`, specifying where your kitchen
|
53
|
+
will be located, and a copy of the kitchen itself. In the future, we will support specifying
|
54
|
+
deployment-related configuration in this file (such as SSH information). So we recommend you keep it
|
55
|
+
gitignored if it contains sensitive information. The kitchen should be kept under version control,
|
56
|
+
and you can either fork it prior to running `amoeba init` or you can let `amoeba init` make a copy
|
57
|
+
of our skeleton which you can add to version control later.
|
58
|
+
|
59
|
+
## Vagrant Testing
|
60
|
+
|
61
|
+
Using Vagrant to test your nodes is easy! Just install Vagrant, run `amoeba init` and modify the
|
62
|
+
kitchen's `Vagrantfile`, as necessary. Run `vagrant up` and watch as your VM comes alive.
|
63
|
+
|
64
|
+
Then, you can ensure the provider is setup correctly (see `data_bags/providers/vagrant.json`). You
|
65
|
+
must ensure the SSH port matches that in `Vagrantfile` (by default 2222), and you must point the
|
66
|
+
provider to the SSH key Vagrant uses (if you just installed Vagrant, this is by default correct,
|
67
|
+
`~/.vagrant.d/insecure_private_key`). You can, however, change the private key (for security
|
68
|
+
reasons if you plan on distributing the VM or using it in production) by specifying an alternative
|
69
|
+
one in the `Vagrantfile` configuration key `config.ssh.private_key_path` (see
|
70
|
+
[the following documentation](http://docs-v1.vagrantup.com/v1/docs/config/ssh/private_key_path.html)
|
71
|
+
on Vagrant for more information).
|
72
|
+
|
73
|
+
## Commands
|
74
|
+
|
75
|
+
### amoeba init [url] [--skeleton]
|
76
|
+
|
77
|
+
The URL is optional, but if specified will be used as the library's git repo. You can also specify
|
78
|
+
`--skeleton`, which will use the specified URL as a skeleton, and make a copy of it (useful if you
|
79
|
+
do not already have a Kitchen you wish to use in git, but want to start a new one based on the URL
|
80
|
+
specified).
|
81
|
+
|
82
|
+
The default URL is [Amoeba's skeleton](https://github.com/AmoebaConsulting/amoeba-kitchen-skel),
|
83
|
+
which will be copied (as if `--skeleton` was specified).
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'amoeba_deploy_tools/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'amoeba_deploy_tools'
|
8
|
+
s.version = AmoebaDeployTools::VERSION
|
9
|
+
s.authors = ['Daniel Jabbour', 'Hike Danakian']
|
10
|
+
s.email = 'sayhi@amoe.ba'
|
11
|
+
s.summary = 'Easy Chef Solo / Knife operation with Amoeba Deploy Tools.'
|
12
|
+
s.description = 'Easy Chef Solo / Knife operation with Amoeba Deploy Tools.'
|
13
|
+
s.homepage = 'https://github.com/AmoebaConsulting/amoeba-deploy-tools'
|
14
|
+
s.license = 'MIT'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
|
21
|
+
s.required_ruby_version = '>= 1.9.2'
|
22
|
+
|
23
|
+
s.add_development_dependency 'bundler', '~> 1.3'
|
24
|
+
s.add_development_dependency 'rake'
|
25
|
+
s.add_development_dependency 'rspec', '~> 2.14.1'
|
26
|
+
s.add_development_dependency 'pry', '~> 0.9.12.4'
|
27
|
+
|
28
|
+
s.add_dependency 'hashie', '~> 2.0.5'
|
29
|
+
s.add_dependency 'thor', '~> 0.18.1'
|
30
|
+
s.add_dependency 'cocaine', '~> 0.5.3'
|
31
|
+
|
32
|
+
s.add_dependency 'chef', '~> 11.8.0'
|
33
|
+
s.add_dependency 'librarian-chef', '~> 0.0.2'
|
34
|
+
s.add_dependency 'knife-solo', '~> 0.4.0'
|
35
|
+
s.add_dependency 'knife-solo_data_bag', '~> 0.4.0'
|
36
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'amoeba_deploy_tools/capistrano/common'
|
2
|
+
|
3
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
4
|
+
_cset(:database_yml_path){ "#{shared_path}/config/database.yml" }
|
5
|
+
|
6
|
+
namespace :amoeba_deploy_tools do
|
7
|
+
desc 'Link the shared/config files into the current/config dir.'
|
8
|
+
task :symlink_configs, :roles => :app do
|
9
|
+
run [ "cd #{latest_release}",
|
10
|
+
"ln -nfs #{database_yml_path} config/"
|
11
|
+
].join(' && ')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'capistrano/version'
|
2
|
+
|
3
|
+
if defined?(Capistrano::VERSION) && Capistrano::VERSION >= '3.0'
|
4
|
+
raise 'We do not yet support Capistrano v3.0. Please downgrade, send us a pull request, or symlink database.yml yourself.'
|
5
|
+
else
|
6
|
+
require 'amoeba_deploy_tools/capistrano/recipes'
|
7
|
+
|
8
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
9
|
+
after 'deploy:finalize_update', 'amoeba_deploy_tools:symlink_configs'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'hashie/mash'
|
3
|
+
|
4
|
+
module AmoebaDeployTools
|
5
|
+
class Command < Thor
|
6
|
+
|
7
|
+
include AmoebaDeployTools::Concerns::Hooks
|
8
|
+
|
9
|
+
option :node, desc: 'name of the node you wish to operate on (set default in .amoeba.yml)'
|
10
|
+
option :'log-level', desc: 'log level to output (DEBUG, INFO, WARN (default), or ERROR)'
|
11
|
+
option :dry, type: :boolean, default: false, desc: 'Don\'t actually execute any chef commands (just show what would be run)'
|
12
|
+
|
13
|
+
# Note that all subcommands will inherit this class. This means any setup done in here
|
14
|
+
# will be duplicated if it runs at initialization (since the main command and subcommand are
|
15
|
+
# both evaluated at runtime). Thus, it's important not to put anything in the constructor.
|
16
|
+
# If you wish to setup any global state, do so in the Amoeba class initializer.
|
17
|
+
def initialize(args=[], options={}, config={})
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
no_commands do
|
22
|
+
def config
|
23
|
+
return @amoebaConfig if @amoebaConfig
|
24
|
+
|
25
|
+
@amoebaConfig = Config.load('.amoeba.yml')
|
26
|
+
end
|
27
|
+
|
28
|
+
def kitchen_path
|
29
|
+
return @kitchen if @kitchen
|
30
|
+
|
31
|
+
if config.kitchen_.path
|
32
|
+
@kitchen = config.kitchen.path
|
33
|
+
else
|
34
|
+
@kitchen = '.'
|
35
|
+
logger.warn 'Using local dir as kitchen path, no `.amoeba.yml` config found. Consider running `amoeba init`'
|
36
|
+
end
|
37
|
+
|
38
|
+
say_fatal "ERROR: Could not find amoeba kitchen: #{@kitchen}" unless Dir.exists? @kitchen
|
39
|
+
|
40
|
+
@kitchen
|
41
|
+
end
|
42
|
+
|
43
|
+
def inside_kitchen(&block)
|
44
|
+
Bundler.with_clean_env do
|
45
|
+
Dir.chdir(kitchen_path) { block.call }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# The node must be specified unless you set a default one. You can specify it with the
|
50
|
+
# `--node [name]` option, or by setting `[:node][:default]` in your `.amoeba.yml``
|
51
|
+
def node
|
52
|
+
return @node if @node
|
53
|
+
|
54
|
+
node_name = options[:node] || config.node_.default_
|
55
|
+
say_fatal 'ERROR: must specify --node or have a default node in your config file' unless node_name
|
56
|
+
|
57
|
+
inside_kitchen do
|
58
|
+
node_filename = File.expand_path(File.join('nodes', "#{node_name}.json"))
|
59
|
+
if node_name.nil? || !File.exists?(node_filename)
|
60
|
+
say_fatal "ERROR: Could not find node JSON file: #{node_filename}"
|
61
|
+
end
|
62
|
+
|
63
|
+
@node = Config.load(node_filename, format: :json)
|
64
|
+
@node.tap {|n| n.filename = node_filename } if @node
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def remote_node
|
69
|
+
data_bag(:nodes)[node.name]
|
70
|
+
end
|
71
|
+
|
72
|
+
def data_bag(name)
|
73
|
+
DataBag.new(name, kitchen_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def deployment
|
77
|
+
return @deployment if @deployment
|
78
|
+
|
79
|
+
@deployment = Hashie::Mash.new
|
80
|
+
@deployment.deep_merge!(node.deployment) if node.deployment
|
81
|
+
|
82
|
+
provider = data_bag(:providers)[node.deployment.provider] if node.deployment_.provider
|
83
|
+
|
84
|
+
@deployment.deep_merge!(provider) if provider
|
85
|
+
@deployment.deep_merge!(remote_node.deployment) if remote_node.deployment
|
86
|
+
@deployment.deep_merge!(node.deployment)
|
87
|
+
|
88
|
+
return @deployment
|
89
|
+
end
|
90
|
+
|
91
|
+
def logger
|
92
|
+
Logger.instance
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_chef_id!(name)
|
96
|
+
say_fatal "You must specify a key name for your data bag id." unless name
|
97
|
+
unless name =~ /^[a-zA-Z0-9\_\-]+$/
|
98
|
+
say_fatal "Your data bag name must only contain alphanums, dashes, and underscores. `#{name}` is invalid!"
|
99
|
+
end
|
100
|
+
return true
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module AmoebaDeployTools
|
2
|
+
|
3
|
+
DEFAULT_SKELETON_REPO = 'https://github.com/AmoebaConsulting/amoeba-kitchen-skel.git'
|
4
|
+
|
5
|
+
class Amoeba < Command
|
6
|
+
|
7
|
+
# Any "global" setup can be done here, as the "amoeba" command will always be initialized
|
8
|
+
def initialize(args=[], options={}, config={})
|
9
|
+
super
|
10
|
+
setup_logger
|
11
|
+
setup_cocaine
|
12
|
+
|
13
|
+
# Fix JSON so it doesn't try to dereference json_class in JSON responses
|
14
|
+
# See http://blog.defunct.ca/2013/02/01/query-chef-server-api-from-ruby-script/
|
15
|
+
JSON.create_id = ''
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'init (url optional)', 'Setup Amoeba Deploy Tools (either by creating a new kitchen or locating an existing one)'
|
19
|
+
method_options :skeleton => :boolean
|
20
|
+
def init(url=nil)
|
21
|
+
# Store the user-specified URL if it exists
|
22
|
+
user_url = url if url
|
23
|
+
|
24
|
+
# Check if we're in a git repo
|
25
|
+
if system('git rev-parse > /dev/null 2>&1')
|
26
|
+
project_dir = %x{git rev-parse --show-toplevel}
|
27
|
+
|
28
|
+
default_kitchen_dir = File.expand_path("#{File.basename(project_dir).chop}-kitchen",
|
29
|
+
File.expand_path('..', project_dir))
|
30
|
+
else
|
31
|
+
default_kitchen_dir = File.expand_path('.', 'kitchen')
|
32
|
+
end
|
33
|
+
|
34
|
+
kitchen_dir = ask "Where should the new kitchen be located? (default: #{default_kitchen_dir})"
|
35
|
+
kitchen_dir = default_kitchen_dir if kitchen_dir.empty?
|
36
|
+
|
37
|
+
if File.exist?(kitchen_dir)
|
38
|
+
say 'Existing kitchen found! Will not overwrite.', :yellow
|
39
|
+
else
|
40
|
+
# Copy (not clone) the repo if the URL isn't specified. If it is, obey the --skeleton param
|
41
|
+
copy = url ? options[:skeleton] : true
|
42
|
+
|
43
|
+
# If there was no specified URL, use default one
|
44
|
+
url ||= DEFAULT_SKELETON_REPO
|
45
|
+
|
46
|
+
git_opts = copy ? '--depth 1' : ''
|
47
|
+
if system("git clone #{git_opts} #{url} #{kitchen_dir}")
|
48
|
+
if copy
|
49
|
+
git_dir = File.expand_path('.git', kitchen_dir)
|
50
|
+
if File.directory?(git_dir)
|
51
|
+
FileUtils.rm_rf(git_dir)
|
52
|
+
end
|
53
|
+
say_bold "New kitchen created at: #{kitchen_dir}. Please add it to version control"
|
54
|
+
else
|
55
|
+
say_bold "Kitchen from #{url} has been 'git clone'-ed into your kitchen directory"
|
56
|
+
end
|
57
|
+
else
|
58
|
+
say_fatal "ERROR: Kitchen directory cannot be cloned from URL #{url}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Okay, the kitchen exists (one way or another)
|
63
|
+
|
64
|
+
config.kitchen!.url = user_url if user_url && !options[:skeleton]
|
65
|
+
config.kitchen!.path = kitchen_dir.to_s
|
66
|
+
config.save
|
67
|
+
|
68
|
+
say_bold 'Saving ./amoeba.yml config file. We suggest you `git ignore` this (contains local settings).'
|
69
|
+
end
|
70
|
+
|
71
|
+
desc 'sync OPTS', 'Not yet implemented.'
|
72
|
+
def sync
|
73
|
+
end
|
74
|
+
|
75
|
+
desc 'update OPTS', 'Not yet implemented.'
|
76
|
+
def update
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'app [COMMAND]', 'Manage the deployed application (see `amoeba app help`)'
|
80
|
+
subcommand 'app', AmoebaDeployTools::App
|
81
|
+
|
82
|
+
desc 'node [COMMAND]', 'Deploy and configure nodes (see `amoeba node help`)'
|
83
|
+
subcommand 'node', AmoebaDeployTools::Node
|
84
|
+
|
85
|
+
desc 'key [COMMAND]', 'Manage private keys (see `amoeba key help`)'
|
86
|
+
subcommand 'key', AmoebaDeployTools::Key
|
87
|
+
|
88
|
+
desc 'ssl [COMMAND]', 'Manage SSL certificates (see `amoeba ssl help`)'
|
89
|
+
subcommand 'ssl', AmoebaDeployTools::Ssl
|
90
|
+
|
91
|
+
no_commands do
|
92
|
+
def setup_logger
|
93
|
+
# Default logging level is warn. You can change this in your .amoeba.yml config
|
94
|
+
# by setting `logLevel` or by passing --logLevel option
|
95
|
+
level = 'WARN'
|
96
|
+
level = config.log_level if config.log_level
|
97
|
+
level = options[:'log-level'] if options[:'log-level']
|
98
|
+
begin
|
99
|
+
level = AmoebaDeployTools::Logger.const_get level.upcase
|
100
|
+
rescue NameError
|
101
|
+
say "WARNING: Invalid log level: #{level}. Defaulting to WARN.", :red
|
102
|
+
level = AmoebaDeployTools::Logger::WARN
|
103
|
+
end
|
104
|
+
AmoebaDeployTools::Logger.instance.level = level
|
105
|
+
end
|
106
|
+
|
107
|
+
def setup_cocaine
|
108
|
+
if options[:dry]
|
109
|
+
Cocaine::CommandLine.runner = Cocaine::CommandLine::FakeRunner.new
|
110
|
+
else
|
111
|
+
Cocaine::CommandLine.runner = AmoebaDeployTools::NoiseyCocaineRunner.new
|
112
|
+
end
|
113
|
+
|
114
|
+
Cocaine::CommandLine.logger = AmoebaDeployTools::Logger.instance
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module AmoebaDeployTools
|
2
|
+
class App < Command
|
3
|
+
desc 'deploy', 'Deploy the application (using capistrano)'
|
4
|
+
def deploy
|
5
|
+
cap :deploy
|
6
|
+
end
|
7
|
+
|
8
|
+
desc 'capfile', 'Generate Capfile for application'
|
9
|
+
def capfile
|
10
|
+
app = node.application
|
11
|
+
sudo(node.name, "cat ~#{app.name}/shared/config/Capfile")
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'exec CMD', "executes CMD on remote server in app's env"
|
15
|
+
def exec
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'ssh', 'SSHs to the application node, as the application user'
|
19
|
+
def ssh
|
20
|
+
end
|
21
|
+
|
22
|
+
no_commands do
|
23
|
+
def cap(cmd)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module AmoebaDeployTools
|
2
|
+
module Concerns
|
3
|
+
module Hooks
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
### Class methods
|
10
|
+
def before_hooks
|
11
|
+
@before_hooks ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_hooks
|
15
|
+
@after_hooks ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def before(&blk)
|
19
|
+
@before_hooks ||= []
|
20
|
+
@before_hooks << blk
|
21
|
+
end
|
22
|
+
|
23
|
+
def after(&blk)
|
24
|
+
@after_hooks ||= []
|
25
|
+
@after_hooks << blk
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
#### Instance methods
|
30
|
+
|
31
|
+
def invoke_command(command, *args)
|
32
|
+
# Ignore hooks on help commands
|
33
|
+
if command.name == 'help'
|
34
|
+
return super
|
35
|
+
end
|
36
|
+
|
37
|
+
self.class.before_hooks.each {|h| instance_eval &h }
|
38
|
+
retVal = super
|
39
|
+
self.class.after_hooks.each {|h| instance_eval &h }
|
40
|
+
return retVal
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module AmoebaDeployTools
|
2
|
+
module Concerns
|
3
|
+
module SSH
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
### Class methods
|
10
|
+
end
|
11
|
+
|
12
|
+
### Instance methods
|
13
|
+
|
14
|
+
# Outputs SSH options for connecting to this node (provide a map of deploy key to command
|
15
|
+
# line arg name).
|
16
|
+
def node_host_args(flag_map)
|
17
|
+
say_fatal 'ERROR: Missing deployment info for node.' unless deployment && deployment.host
|
18
|
+
|
19
|
+
host_arg = deployment.host
|
20
|
+
host_arg = "#{deployment.user}@#{host_arg}" if deployment.user
|
21
|
+
|
22
|
+
# Iterate through all the specified flags and check if they're defined in the deployment
|
23
|
+
# config, appending them to the output if they are.
|
24
|
+
flag_map.each do |field, argument_name|
|
25
|
+
host_arg << " #{argument_name} #{deployment[field]}" if deployment[field]
|
26
|
+
end
|
27
|
+
|
28
|
+
host_arg
|
29
|
+
end
|
30
|
+
|
31
|
+
# Run knife solo command on server
|
32
|
+
def knife_solo(cmd, options={})
|
33
|
+
say_fatal 'ERROR: Node must have a name defined' unless node.name
|
34
|
+
|
35
|
+
# Ensure json is a hashie
|
36
|
+
json = options.delete(:json) || Hashie::Mash.new
|
37
|
+
|
38
|
+
# If a block is specified, it means we have json in it, so let's resolve it
|
39
|
+
yield(json) if block_given?
|
40
|
+
|
41
|
+
# Ensure JSON for the node is set. It is either provided by the calling party, or we use the
|
42
|
+
# node file's definitions.
|
43
|
+
if json.empty?
|
44
|
+
json.deep_merge!(node)
|
45
|
+
end
|
46
|
+
|
47
|
+
exec = "bundle exec knife solo #{cmd.to_s} "
|
48
|
+
|
49
|
+
if options.delete(:ssh)
|
50
|
+
exec << node_host_args(port: '--ssh-port',
|
51
|
+
config: '--ssh-config-file',
|
52
|
+
ident: '--identity-file') << ' '
|
53
|
+
exec << "--no-host-key-verify --node-name #{node.name}"
|
54
|
+
end
|
55
|
+
|
56
|
+
if options.delete(:include_private_key)
|
57
|
+
private_key = node.private_key || 'default'
|
58
|
+
private_key = config.private_keys_[private_key]
|
59
|
+
|
60
|
+
# Stick the private key into a our custom JSON (to be appended to the node)
|
61
|
+
json.private_key_raw = private_key if private_key
|
62
|
+
end
|
63
|
+
|
64
|
+
opts = {}
|
65
|
+
opts[:runner] = AmoebaDeployTools::InteractiveCocaineRunner.new if options.delete(:interactive)
|
66
|
+
|
67
|
+
# Now go through all the options specified and append them to args
|
68
|
+
args = ''
|
69
|
+
options.each do |argument, value|
|
70
|
+
args << " --#{argument} #{value}"
|
71
|
+
end
|
72
|
+
|
73
|
+
inside_kitchen do
|
74
|
+
# JSON will be written to a temp file and used in place of the node JSON file
|
75
|
+
with_tmpfile(JSON.dump(json), name: ['node', '.json']) do |file_name|
|
76
|
+
knife_solo_cmd = Cocaine::CommandLine.new(exec, "#{args} #{file_name}", opts)
|
77
|
+
knife_solo_cmd.run
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def ssh_run(cmd, options)
|
83
|
+
options = {
|
84
|
+
silent: false,
|
85
|
+
interactive: false
|
86
|
+
}.merge!(options)
|
87
|
+
|
88
|
+
opts = {}
|
89
|
+
opts[:runner] = Cocaine::CommandLine::BackticksRunner.new if options[:silent]
|
90
|
+
opts[:runner] = AmoebaDeployTools::InteractiveCocaineRunner.new if options[:interactive]
|
91
|
+
|
92
|
+
ssh_cmd = node_host_args(port: '-p', ident: '-i')
|
93
|
+
|
94
|
+
[ 'Compression=yes',
|
95
|
+
'DSAAuthentication=yes',
|
96
|
+
'LogLevel=FATAL',
|
97
|
+
'StrictHostKeyChecking=no',
|
98
|
+
'UserKnownHostsFile=/dev/null'
|
99
|
+
].each do |opt|
|
100
|
+
ssh_cmd << " -o #{opt}"
|
101
|
+
end
|
102
|
+
|
103
|
+
ssh_cmd << " '#{cmd}'" if cmd && !cmd.empty?
|
104
|
+
|
105
|
+
Cocaine::CommandLine.new('ssh', ssh_cmd, opts).run
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AmoebaDeployTools
|
2
|
+
class Key < Command
|
3
|
+
|
4
|
+
desc 'create', 'Create a private_key (used to encrypt secret data_bags like SSL certs)'
|
5
|
+
def create(name=nil)
|
6
|
+
validate_chef_id!(name)
|
7
|
+
|
8
|
+
key = Cocaine::CommandLine.new('openssl', "rand -base64 512 | tr -d '\\r\\n'",
|
9
|
+
runner: Cocaine::CommandLine::BackticksRunner.new).run
|
10
|
+
config.private_keys![name] = key
|
11
|
+
|
12
|
+
logger.debug "Saving key to `.amoeba.yml` config"
|
13
|
+
|
14
|
+
if config.new_file?
|
15
|
+
say_fatal "Cannot create new key, no .amoeba.yml file found! Please run `amoeba init`"
|
16
|
+
end
|
17
|
+
|
18
|
+
config.save
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|