master-cap 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/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/Readme.markdown ADDED
@@ -0,0 +1,101 @@
1
+ # What is it ?
2
+
3
+ Here are [capistrano](https://github.com/capistrano/capistrano/wiki) extensions to be used with [master-chef](https://github.com/octo-technology/master-chef)
4
+
5
+ # How to use it
6
+
7
+ * Add ``master-cap`` to your Gemfile
8
+ * Load ``master-cap`` in your Capfile: ``require 'master-cap/topology-directory.rb'``
9
+ * Create a subdirectory named ``topology``, and add your toplogy YAML files into
10
+ * Enjoy :)
11
+
12
+ # Topology file
13
+
14
+ Example of a topology file: ``integ.yml``
15
+
16
+ ```yml
17
+ :topology:
18
+ :app:
19
+ :hostname: my_app_server.mydomain.net
20
+ :type: linux_chef
21
+ :roles:
22
+ - app_server
23
+ :db:
24
+ :hostname: db_server.mydomain.net
25
+ :type: linux_chef
26
+ :roles:
27
+ - db_server
28
+ :redis:
29
+ :hostname: redise.mydomain.net
30
+ :type: linux_chef
31
+ :roles:
32
+ - redis_server
33
+ :cap_override:
34
+ :my_specific_cap_param: 'toto'
35
+ :default_role_list:
36
+ - base
37
+ ```
38
+
39
+ # Capistrano commands
40
+
41
+ ## Node selection
42
+
43
+ * ``cap integ show``
44
+ * ``cap app-integ show``
45
+ * ``cap integ_db_server show``
46
+
47
+ ## SSH command
48
+
49
+ * ``cap integ check``: try to connect on each nodes with ssh
50
+ * ``cap integ ssh_cmd -s cmd=uname``: exec ``uname``command on each nodes
51
+
52
+ # Master-chef binding
53
+
54
+ ## Configuration
55
+
56
+ If you use only master-chef (no custom repo with Chef recipes), you have nothing to do.
57
+
58
+ If you use a custom repo, please add following lines before requiring master-cap
59
+
60
+ ```ruby
61
+ set :git_repos_manager_class, 'SimpleGitReposManager'
62
+
63
+ set :git_repos, [
64
+ {
65
+ :url => "http://github.com/octo-technology/master-chef.git",
66
+ :ref => "aa597911b6d394dff27338c825aa966105cb6607",
67
+ },
68
+ {
69
+ :url => "git@github.com:xxxx/yyyy.git",
70
+ :local_path => "../yyyy",
71
+ }
72
+ ]
73
+ ```
74
+
75
+ Note:
76
+ * ``:ref`` is used to lock master-chef version
77
+ * ``:local_path`` is used to run ``chef:local``
78
+
79
+ # Cap commands
80
+
81
+ * ``cap integ chef``: run chef on all servers
82
+ * ``cap app-integ chef:local``: run chef on app server, using local recipes. Usefull for debugging.
83
+ * ``cap app-integ chef:generate_local_json``: generate the ``local.json`` file using current topology.
84
+ * ``cap app-integ chef:stack``: output last chef stack trace
85
+ * ``cap integ chef:stack``: purge all master-chef git repos caches
86
+
87
+ # License
88
+
89
+ Copyright 2012 Bertrand Paquet
90
+
91
+ Licensed under the Apache License, Version 2.0 (the "License");
92
+ you may not use this file except in compliance with the License.
93
+ You may obtain a copy of the License at
94
+
95
+ http://www.apache.org/licenses/LICENSE-2.0
96
+
97
+ Unless required by applicable law or agreed to in writing, software
98
+ distributed under the License is distributed on an "AS IS" BASIS,
99
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
100
+ See the License for the specific language governing permissions and
101
+ limitations under the License.
@@ -0,0 +1,105 @@
1
+ require 'tempfile'
2
+ require 'json'
3
+
4
+ require File.join(File.dirname(__FILE__), 'git_repos_manager.rb')
5
+
6
+ Capistrano::Configuration.instance.load do
7
+
8
+ namespace :chef do
9
+
10
+ set :master_chef_path, fetch(:master_chef_path, '../master-chef')
11
+ set :git_repos_manager, Object.const_get(fetch(:git_repos_manager_class, 'EmptyGitReposManager')).new(self)
12
+
13
+ task :generate_local_json do
14
+ set :user, chef_user
15
+ env = check_only_one_env
16
+ find_servers(:roles => chef_role).each do |s|
17
+ env, node = find_node s.host
18
+ r = []
19
+ r += (TOPOLOGY[env][:default_role_list] || []) unless node[:no_default_role]
20
+ r += node[:roles]
21
+ json = JSON.pretty_generate({
22
+ :repos => {
23
+ :git => git_repos_manager.list,
24
+ },
25
+ :run_list => r.map{|x| "role[#{x}]"},
26
+ :node_config => {
27
+ :topology_node_name => node[:topology_name]
28
+ }
29
+ })
30
+ puts json
31
+ f = Tempfile.new File.basename("local_json_#{name}")
32
+ f.write json
33
+ f.close
34
+ upload f.path, "/etc/chef/local.json", {:hosts => [s]}
35
+ end
36
+ git_repos_manager.list.each do |git_repo|
37
+ if git_repo =~ /^.+@.+:.+\.git$/
38
+ run "sudo ssh -o StrictHostKeyChecking=no #{git_repo.split(':')[0]} echo toto || true > /dev/null 2>&1", :roles => chef_role
39
+ end
40
+ end
41
+ end
42
+
43
+ def get_prefix
44
+ prefix = ""
45
+ prefix += "http_proxy=#{http_proxy} https_proxy=#{http_proxy}" if exists? :http_proxy
46
+ prefix
47
+ end
48
+
49
+ task :upload_git_tag_override, :roles => :linux_chef do
50
+ set :user, chef_user
51
+ env = check_only_one_env
52
+
53
+ git_tag_override = git_repos_manager.compute_override(env)
54
+
55
+ if git_tag_override
56
+ f = Tempfile.new File.basename("git_tag_override")
57
+ f.write JSON.dump(git_tag_override)
58
+ f.close
59
+
60
+ upload_to_root f.path, "/etc/chef/local.json.git_tag_override"
61
+ end
62
+
63
+ end
64
+
65
+ task :upload_topology, :roles => :linux_chef do
66
+ set :user, chef_user
67
+ env = check_only_one_env
68
+
69
+ f = Tempfile.new File.basename("topology_env")
70
+ f.write YAML.dump(TOPOLOGY[env])
71
+ f.close
72
+ upload_to_root f.path, "/etc/chef/topology.yml"
73
+ end
74
+
75
+ task :default, :roles => chef_role do
76
+ set :user, chef_user
77
+ upload_topology
78
+ upload_git_tag_override
79
+ run "#{get_prefix} /etc/chef/update.sh"
80
+ end
81
+
82
+ task :stack, :roles => chef_role do
83
+ set :user, chef_user
84
+ run "sudo cat /var/chef/cache/chef-stacktrace.out"
85
+ end
86
+
87
+ task :purge_cache, :roles => chef_role do
88
+ set :user, chef_user
89
+ run "sudo rm -rf /var/chef/cache/git_repos"
90
+ end
91
+
92
+ task :local, :roles => chef_role do
93
+ set :user, chef_user
94
+ upload_topology
95
+ find_servers(:roles => chef_role).each do |x|
96
+ prefix = ""
97
+ prefix = "PROXY=#{http_proxy}" if exists? :http_proxy
98
+ command = "sh -c \"#{prefix} #{master_chef_path}/runtime/chef_local.rb #{x} #{git_repos_manager.compute_local_path}\""
99
+ abort unless system command
100
+ end
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,14 @@
1
+
2
+ class DefaultTranslationStrategy
3
+
4
+ def node_name env, name, node, topology
5
+ return name.to_s if topology[:no_node_suffix]
6
+ "#{name}-#{env}"
7
+ end
8
+
9
+ def node_hostname env, name, node, topology
10
+ return node[:hostname] if node[:hostname]
11
+ raise "No hostname in #{node}"
12
+ end
13
+
14
+ end
@@ -0,0 +1,48 @@
1
+
2
+ class EmptyGitReposManager
3
+
4
+ def initialize cap
5
+ @cap = cap
6
+ end
7
+
8
+ def compute_override env
9
+ if @cap.exists? :master_chef_version
10
+ return {"http://github.com/octo-technology/master-chef.git" => @cap.master_chef_version}
11
+ end
12
+ nil
13
+ end
14
+
15
+ def list
16
+ ["http://github.com/octo-technology/master-chef.git"]
17
+ end
18
+
19
+ def compute_local_path
20
+ ""
21
+ end
22
+
23
+ end
24
+
25
+ class SimpleGitReposManager
26
+
27
+ def initialize cap
28
+ @cap = cap
29
+ @repos = @cap.fetch(:git_repos, [])
30
+ end
31
+
32
+ def compute_override env
33
+ result = {}
34
+ @repos.each do |x|
35
+ result[x[:url]] = x[:ref] if x[:ref]
36
+ end
37
+ result.size == 0 ? nil : result
38
+ end
39
+
40
+ def list
41
+ @repos.map{|x| x[:url]}
42
+ end
43
+
44
+ def compute_local_path
45
+ @repos.map{|x| x[:local_path] ? File.expand_path(x[:local_path]) : ""}.join(' ')
46
+ end
47
+
48
+ end
@@ -0,0 +1,90 @@
1
+
2
+ require 'timeout'
3
+
4
+ unless Capistrano::Configuration.respond_to?(:instance)
5
+ abort "master-cap requires Capistrano 2"
6
+ end
7
+
8
+ Capistrano::Configuration.instance.load do
9
+
10
+ def exec_local_with_timeout cmd, timeout
11
+ pid = Process.fork { exec cmd }
12
+ begin
13
+ Timeout.timeout(timeout) do
14
+ Process.wait(pid)
15
+ raise "Wrong return code for #{cmd} : #{$?.exitstatus}" unless $?.exitstatus == 0
16
+ end
17
+ rescue Timeout::Error
18
+ Process.kill('TERM', pid)
19
+ raise "Timeout when executing #{cmd}"
20
+ end
21
+ end
22
+
23
+ def exec_local cmd
24
+ begin
25
+ abort "#{cmd} failed. Aborting..." unless system cmd
26
+ rescue
27
+ abort "#{cmd} failed. Aborting..."
28
+ end
29
+ end
30
+
31
+ def capture_local cmd
32
+ begin
33
+ result = %x{#{cmd}}
34
+ abort "#{cmd} failed. Aborting..." unless $? == 0
35
+ result
36
+ rescue
37
+ abort "#{cmd} failed. Aborting..."
38
+ end
39
+ end
40
+
41
+ def upload_to_root local, remote, options = {}
42
+ tmp_file = "/tmp/#{File.basename(remote)}"
43
+ upload local, tmp_file, options
44
+ run_root "mv #{tmp_file} #{remote}"
45
+ end
46
+
47
+ def run_root cmd
48
+ run "sudo sh -c '#{cmd}'"
49
+ end
50
+
51
+ def error msg
52
+ abort "Error : #{msg}"
53
+ end
54
+
55
+ def fill s, k
56
+ s.length >= k ? s : fill(s.to_s + " ", k)
57
+ end
58
+
59
+ def multiple_capture command, options = nil
60
+ result = {}
61
+ launch = true
62
+ launch = false if options && options[:hosts] && options[:hosts] == []
63
+ if launch
64
+ begin
65
+ parallel(options) do |session|
66
+ session.else command do |channel, stream, data|
67
+ env, name, node = find_node(channel.properties[:server].host)
68
+ if block_given?
69
+ result[name] = yield name, node, data
70
+ else
71
+ result[name] = data
72
+ end
73
+ end
74
+ end
75
+ rescue Capistrano::ConnectionError => e
76
+ puts "\e[31m#{e}\e[0m"
77
+ end
78
+ end
79
+ if options
80
+ default_value = options[:default_value]
81
+ if default_value
82
+ find_servers.map{|x| find_node x.host}.each do |env, name, node|
83
+ result[name] = default_value unless result[name]
84
+ end
85
+ end
86
+ end
87
+ result
88
+ end
89
+
90
+ end
@@ -0,0 +1,64 @@
1
+
2
+ require File.join(File.dirname(__FILE__), 'topology.rb')
3
+
4
+ Capistrano::Configuration.instance.load do
5
+
6
+ topology_directory = fetch(:topology_directory, 'topology')
7
+
8
+ task :load_topology do
9
+ Dir["#{topology_directory}/*.yml"].each do |f|
10
+ env = File.basename(f).split('.')[0]
11
+ TOPOLOGY[env] = YAML.load(File.read(f))
12
+ nodes = []
13
+ roles_map = {}
14
+ default_role_nodes = []
15
+ TOPOLOGY[env][:topology].each do |k, v|
16
+ v[:topology_name] = translation_strategy.node_name(env, k, v, TOPOLOGY[env])
17
+ v[:topology_hostname] = begin translation_strategy.node_hostname(env, k, v, TOPOLOGY[env]) rescue nil end
18
+ next unless v[:topology_hostname]
19
+ node_roles = v[:roles].map{|x| x.to_sym}
20
+ node_roles << v[:type].to_sym if v[:type]
21
+ n = {:name => k.to_s, :host => v[:topology_hostname], :roles => node_roles}
22
+ nodes << n
23
+ default_role_nodes << n unless v[:no_default_role]
24
+ task v[:topology_name] do
25
+ server n[:host], *node_roles if n[:host]
26
+ load_cap_override env
27
+ end
28
+ node_roles.each do |r|
29
+ roles_map[r] = [] unless roles_map[r]
30
+ roles_map[r] << n
31
+ end
32
+ end
33
+ roles_map.each do |r, v|
34
+ task "#{env}_#{r}" do
35
+ v.each do |k|
36
+ server k[:host], *(k[:roles]) if k[:host]
37
+ load_cap_override env
38
+ end
39
+ end
40
+ end
41
+ task "#{env}_default_role" do
42
+ default_role_nodes.each do |k|
43
+ server k[:host], *(k[:roles])
44
+ load_cap_override env
45
+ end
46
+ end
47
+ task env do
48
+ nodes.each do |k|
49
+ server k[:host], *(k[:roles]) if k[:host]
50
+ load_cap_override env
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ on :load, "load_topology"
57
+
58
+ def load_cap_override env
59
+ TOPOLOGY[env][:cap_override].each do |k, v|
60
+ set k, v
61
+ end if TOPOLOGY[env][:cap_override]
62
+ end
63
+
64
+ end
@@ -0,0 +1,71 @@
1
+
2
+ require File.join(File.dirname(__FILE__), 'misc.rb')
3
+ require File.join(File.dirname(__FILE__), 'default_translation_strategy.rb')
4
+
5
+ unless Capistrano::Configuration.respond_to?(:instance)
6
+ abort "master-cap requires Capistrano 2"
7
+ end
8
+
9
+ TOPOLOGY = {}
10
+
11
+ Capistrano::Configuration.instance.load do
12
+
13
+ set :chef_role, fetch(:chef_role, :linux_chef)
14
+ set :chef_user, fetch(:chef_user, "chef")
15
+ set :translation_strategy, Object.const_get(fetch(:translation_strategy_class, 'DefaultTranslationStrategy')).new
16
+
17
+ task :check do
18
+ find_servers(:roles => chef_role).sort_by{|s| s.host}.each do |s|
19
+ env, node = find_node s.host
20
+ begin
21
+ exec_local_with_timeout "ssh -o StrictHostKeyChecking=no #{chef_user}@#{node[:topology_hostname]} uname > /dev/null 2>&1", fetch(:check_timeout, 10)
22
+ puts "OK : #{node[:topology_hostname]}"
23
+ rescue
24
+ puts "ERROR : Unable to join #{node[:topology_hostname]}"
25
+ end
26
+ end
27
+ end
28
+
29
+ task :ssh_cmd, :roles => chef_role do
30
+ error "Please specify command with -s cmd=" unless exists? :cmd
31
+ set :user, chef_user
32
+ run cmd
33
+ end
34
+
35
+ task :show do
36
+ ss = find_servers
37
+ puts "Number of selected servers: #{ss.length}"
38
+ ss.each do |s|
39
+ env, node = find_node s.host
40
+ puts "#{fill(node[:topology_name], 30)} #{fill(node[:topology_hostname], 50)} [#{node[:roles].sort.join(',')}]"
41
+ end
42
+ end
43
+
44
+ def find_node node_name
45
+ TOPOLOGY.each do |k, v|
46
+ v[:topology].each do |name, node|
47
+ return [k, node] if translation_strategy.node_hostname(k, name, node, v) == node_name
48
+ end
49
+ end
50
+ error "Node not found #{node_name}"
51
+ end
52
+
53
+ def check_only_one_env servers = nil
54
+ servers = find_servers unless servers
55
+ env_list = {}
56
+ servers.each do |s|
57
+ env, name, node = find_node(s.is_a?(String) ? s : s.host)
58
+ env_list[env] = :toto
59
+ end
60
+ error "Please, do not launch this command without env" if env_list.keys.size == 0
61
+ error "Please, do not launch this command on two env : #{env_list.keys.join(' ')}" if env_list.keys.size != 1
62
+ env = env_list.keys.first
63
+
64
+ check_only_one_env_callback(env, servers) if exists? :check_only_one_env_callback
65
+
66
+ env
67
+ end
68
+
69
+ end
70
+
71
+ require File.join(File.dirname(__FILE__), 'chef.rb')
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+
6
+ s.name = "master-cap"
7
+ s.version = "0.1"
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Bertrand Paquet"]
10
+ s.email = ["bertrand.paquet@gmail.com"]
11
+ s.homepage = "http://github.com/bpaquet/master-cap"
12
+ s.summary = "Capistrano tasks designed to work with master-chef"
13
+ s.description = "Capistrano tasks designed to work with master-chef"
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+ s.extra_rdoc_files = [
19
+ "Readme.markdown"
20
+ ]
21
+
22
+ s.specification_version = 3
23
+ s.add_runtime_dependency(%q<capistrano>, [">= 2"])
24
+ s.add_runtime_dependency(%q<json>)
25
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: master-cap
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Bertrand Paquet
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: capistrano
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
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
+ description: Capistrano tasks designed to work with master-chef
47
+ email:
48
+ - bertrand.paquet@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files:
52
+ - Readme.markdown
53
+ files:
54
+ - Rakefile
55
+ - Readme.markdown
56
+ - lib/master-cap/chef.rb
57
+ - lib/master-cap/default_translation_strategy.rb
58
+ - lib/master-cap/git_repos_manager.rb
59
+ - lib/master-cap/misc.rb
60
+ - lib/master-cap/topology-directory.rb
61
+ - lib/master-cap/topology.rb
62
+ - master-cap.gemspec
63
+ homepage: http://github.com/bpaquet/master-cap
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.24
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Capistrano tasks designed to work with master-chef
87
+ test_files: []