master-cap 0.1

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