dust-deploy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +154 -0
- data/Rakefile +1 -0
- data/bin/dust +215 -0
- data/dust.gemspec +28 -0
- data/lib/dust.rb +6 -0
- data/lib/dust/convert_size.rb +21 -0
- data/lib/dust/print_status.rb +57 -0
- data/lib/dust/recipes/aliases.rb +13 -0
- data/lib/dust/recipes/basic_setup.rb +35 -0
- data/lib/dust/recipes/debsecan.rb +44 -0
- data/lib/dust/recipes/duplicity.rb +111 -0
- data/lib/dust/recipes/etc_hosts.rb +13 -0
- data/lib/dust/recipes/iptables.rb +267 -0
- data/lib/dust/recipes/locale.rb +17 -0
- data/lib/dust/recipes/memory_limit.rb +23 -0
- data/lib/dust/recipes/motd.rb +12 -0
- data/lib/dust/recipes/mysql.rb +49 -0
- data/lib/dust/recipes/nginx.rb +53 -0
- data/lib/dust/recipes/packages.rb +9 -0
- data/lib/dust/recipes/postgres.rb +119 -0
- data/lib/dust/recipes/rc_local.rb +24 -0
- data/lib/dust/recipes/repositories.rb +88 -0
- data/lib/dust/recipes/resolv_conf.rb +42 -0
- data/lib/dust/recipes/ssh_authorized_keys.rb +58 -0
- data/lib/dust/recipes/unattended_upgrades.rb +29 -0
- data/lib/dust/recipes/zabbix_agent.rb +84 -0
- data/lib/dust/server.rb +382 -0
- data/lib/dust/version.rb +3 -0
- metadata +139 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
dust - a ssh/facter only server deployment tool
|
2
|
+
=============
|
3
|
+
|
4
|
+
dust is a deployment tool, kinda like sprinkle. but cooler (at least for me).
|
5
|
+
it's for those, who would like to maintain their servers with a tool like puppet or chef, but are scared by the thought of having configuration files, credentials and other stuff centrally on a server reachable via the internet.
|
6
|
+
|
7
|
+
although the tool is not as versatile and elite as puppet, it's still cool for most use cases, and easily extendable.
|
8
|
+
|
9
|
+
|
10
|
+
installing
|
11
|
+
------------
|
12
|
+
|
13
|
+
installation is quite simple. just
|
14
|
+
gem install dust
|
15
|
+
|
16
|
+
|
17
|
+
using
|
18
|
+
------------
|
19
|
+
|
20
|
+
first, let's start by creating your dust directory
|
21
|
+
|
22
|
+
$ mkdir mynetwork.dust
|
23
|
+
|
24
|
+
then, create directories you might/will need. (there's going to be an automation process in the future, e.g. using "dust new mynetwork.dust")
|
25
|
+
|
26
|
+
$ cd mynetwork.dust
|
27
|
+
$ mkdir templates
|
28
|
+
$ mkdir nodes
|
29
|
+
|
30
|
+
in the nodes directory, there will be your templates and node configurations.
|
31
|
+
dust uses simple .yaml files for configuring your nodes.
|
32
|
+
let's start by adding a simple host:
|
33
|
+
|
34
|
+
$ vi nodes/yourhost.yaml
|
35
|
+
|
36
|
+
and put in basic information:
|
37
|
+
|
38
|
+
# the hostname (fqdn, or set the domain parameter as well, ip also works)
|
39
|
+
# you don't need a password if you connect using ssh keys
|
40
|
+
hostname: yourhost.example.com
|
41
|
+
password: supersecretphrase
|
42
|
+
|
43
|
+
# these are the default values, you have to put them in case you need something else.
|
44
|
+
# be aware: sudo usage is not yet supported, but ssh keys are!
|
45
|
+
port: 22
|
46
|
+
user: root
|
47
|
+
|
48
|
+
# because this alone won't tell dust what to do, let's for example install a package
|
49
|
+
recipes:
|
50
|
+
packages: [ 'vim', 'git-core', 'rsync' ]
|
51
|
+
|
52
|
+
|
53
|
+
you can then save the file, and tell dust to get to work:
|
54
|
+
|
55
|
+
$ dust deploy
|
56
|
+
|
57
|
+
[ yourhost.example.com ]
|
58
|
+
|
59
|
+
|packages|
|
60
|
+
- checking if vim is installed [ ok ]
|
61
|
+
- checking if git-core is installed [ failed ]
|
62
|
+
- installing git-core [ ok ]
|
63
|
+
- checking if rsync is installed [ ok ]
|
64
|
+
|
65
|
+
you should see dust connecting to the node, checking if the requestet packages are installed, and if not, install them.
|
66
|
+
dust works with aptitude, yum and emerge systems at the moment (testet with ubuntu, debian, gentoo, scientificlinux, centos).
|
67
|
+
feel free to contribute to dust, so that your system is also supportet. contribution is easy!
|
68
|
+
|
69
|
+
|
70
|
+
inheritance
|
71
|
+
------------
|
72
|
+
|
73
|
+
because sometimes you will have similar configuration files for multiple systems, you can create templates.
|
74
|
+
i usually start filenames of templates with an underscore, but that's not a must.
|
75
|
+
|
76
|
+
$ vi nodes/_default.yaml
|
77
|
+
|
78
|
+
this template defines some general settings, usually used by most hosts
|
79
|
+
|
80
|
+
domain: example.com
|
81
|
+
port: 22
|
82
|
+
user: root
|
83
|
+
|
84
|
+
|
85
|
+
and another one:
|
86
|
+
|
87
|
+
$ vi nodes/_debian.yaml
|
88
|
+
|
89
|
+
in this template, i put in some debian specific settings
|
90
|
+
|
91
|
+
# you can add custom fields like "group"
|
92
|
+
# and filter on which hosts to deploy later
|
93
|
+
group: debian
|
94
|
+
|
95
|
+
recipes:
|
96
|
+
locale: en_US.UTF-8
|
97
|
+
debsecan: default
|
98
|
+
repositories:
|
99
|
+
default:
|
100
|
+
url: "http://ftp.de.debian.org/debian/"
|
101
|
+
components: "main contrib non-free"
|
102
|
+
|
103
|
+
|
104
|
+
you can then inherit these templates in your yourhost.yaml:
|
105
|
+
|
106
|
+
hostname: yourhost
|
107
|
+
inherits: [ _default, _debian ]
|
108
|
+
|
109
|
+
recipes:
|
110
|
+
packages: [ 'vim', 'git-core', 'rsync' ]
|
111
|
+
|
112
|
+
|
113
|
+
running dust now, will use the inherited settings as well.
|
114
|
+
you can also overwrite settings in the template with the ones in yourhost.yaml
|
115
|
+
|
116
|
+
$ dust deploy
|
117
|
+
|
118
|
+
[ yourhost ]
|
119
|
+
|
120
|
+
|repositories|
|
121
|
+
- determining whether node uses apt [ ok ]
|
122
|
+
- deleting old repositories [ ok ]
|
123
|
+
|
124
|
+
- deploying default repository [ ok ]
|
125
|
+
|
126
|
+
|packages|
|
127
|
+
- checking if vim is installed [ ok ]
|
128
|
+
- checking if git-core is installed [ ok ]
|
129
|
+
- checking if rsync is installed [ ok ]
|
130
|
+
|
131
|
+
|locale|
|
132
|
+
- setting locale to 'en_US.UTF-8' [ ok ]
|
133
|
+
|
134
|
+
|debsecan|
|
135
|
+
- checking if debsecan is installed [ ok ]
|
136
|
+
- configuring debsecan [ ok ]
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
using recipes (and their templates)
|
142
|
+
------------
|
143
|
+
|
144
|
+
|
145
|
+
writing your own recipes
|
146
|
+
------------
|
147
|
+
|
148
|
+
|
149
|
+
contributing
|
150
|
+
------------
|
151
|
+
|
152
|
+
you have a cool contribution or bugfix? yippie! just file in a pull-request!
|
153
|
+
|
154
|
+
### the server.rb methods you can (and should!) use
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/dust
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'thor/runner'
|
5
|
+
require 'thor/util'
|
6
|
+
require 'yaml'
|
7
|
+
require 'dust'
|
8
|
+
|
9
|
+
# stole this from rails
|
10
|
+
# https://github.com/rails/rails/blob/c0262827cacc1baf16668af65c35a09138166394/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
|
11
|
+
class Hash
|
12
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
13
|
+
def deep_merge(other_hash)
|
14
|
+
dup.deep_merge!(other_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
18
|
+
# Modifies the receiver in place.
|
19
|
+
def deep_merge!(other_hash)
|
20
|
+
other_hash.each_pair do |k,v|
|
21
|
+
tv = self[k]
|
22
|
+
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# stole this from Afz902k who posted something similar at stackoverflow.com
|
29
|
+
# adds ability to check if a class with the name of a string exists
|
30
|
+
class String
|
31
|
+
def to_class
|
32
|
+
Kernel.const_get self.capitalize
|
33
|
+
rescue NameError
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_a_defined_class?
|
38
|
+
true if self.to_class
|
39
|
+
rescue NameError
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
module Dust
|
46
|
+
class Deploy < Thor::Runner
|
47
|
+
|
48
|
+
default_task :deploy
|
49
|
+
|
50
|
+
desc 'deploy [server.yaml] [--filter key=value,value2] [--recipes recipe1 recipe2] [--proxy host:port]',
|
51
|
+
'deploy all recipes to the node(s) specified in server.yaml or to all nodes defined in ./nodes/'
|
52
|
+
|
53
|
+
method_options :filter => :hash, :recipes => :array, :proxy => :string,
|
54
|
+
:restart => :boolean, :reload => :boolean
|
55
|
+
|
56
|
+
def deploy yaml=''
|
57
|
+
initialize_thorfiles
|
58
|
+
Dust.print_failed 'no servers match this filter' if load_servers(yaml).empty?
|
59
|
+
|
60
|
+
run_recipes 'deploy'
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
desc 'status [server.yaml] [--filter key=value,value2] [--recipes recipe1 recipe2] [--proxy host:port]',
|
65
|
+
'display status of recipes specified by filter'
|
66
|
+
|
67
|
+
method_options :filter => :hash, :recipes => :array, :proxy => :string
|
68
|
+
|
69
|
+
def status yaml=''
|
70
|
+
initialize_thorfiles
|
71
|
+
Dust.print_failed 'no servers match this filter' if load_servers(yaml).empty?
|
72
|
+
|
73
|
+
run_recipes 'status'
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# run specified recipes in the given context
|
80
|
+
def run_recipes context
|
81
|
+
@nodes.each do |node|
|
82
|
+
recipes = {}
|
83
|
+
|
84
|
+
# skip this node if there are no recipes found
|
85
|
+
next unless node['recipes']
|
86
|
+
|
87
|
+
# generate list of recipes for this node
|
88
|
+
node['recipes'].each do |recipe, ingredients|
|
89
|
+
|
90
|
+
# in case --recipes was set, skip unwanted recipes
|
91
|
+
next unless options[:recipes].include?(recipe) if options[:recipes]
|
92
|
+
|
93
|
+
# skip disabled recipes
|
94
|
+
next if ingredients == 'disabled'
|
95
|
+
|
96
|
+
# check if method and thor task actually exist
|
97
|
+
k = Thor::Util.find_by_namespace recipe
|
98
|
+
next unless k
|
99
|
+
next unless k.method_defined? context
|
100
|
+
|
101
|
+
recipes[recipe] = ingredients
|
102
|
+
end
|
103
|
+
|
104
|
+
# skip this node unless we're actually having recipes to cook
|
105
|
+
next if recipes.empty?
|
106
|
+
|
107
|
+
|
108
|
+
# connect to server
|
109
|
+
server = Server.new node
|
110
|
+
next unless server.connect
|
111
|
+
|
112
|
+
# runs the method with the recipe name, defined and included in recipe/*.rb
|
113
|
+
# call recipes for each recipe that is defined for this node
|
114
|
+
recipes.each do |recipe, ingredients|
|
115
|
+
send recipe, context, server, ingredients, options
|
116
|
+
puts
|
117
|
+
end
|
118
|
+
|
119
|
+
server.disconnect
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# overwrite thorfiles to look for tasks in the recipes directories
|
124
|
+
def thorfiles(relevant_to=nil, skip_lookup=false)
|
125
|
+
Dir[File.dirname(__FILE__) + '/../lib/dust/recipes/*.rb'] | Dir['recipes/*.rb']
|
126
|
+
end
|
127
|
+
|
128
|
+
# loads servers
|
129
|
+
def load_servers yaml=''
|
130
|
+
@nodes = []
|
131
|
+
|
132
|
+
# if the argument is empty, load all yaml files in the ./nodes/ directory
|
133
|
+
# if the argument is a directory, load yaml files in this directory
|
134
|
+
# if the argument is a file, load the file.
|
135
|
+
if yaml.empty?
|
136
|
+
yaml_files = Dir['./nodes/**/*.yaml']
|
137
|
+
else
|
138
|
+
yaml_files = Dir["#{yaml}/**/*.yaml"] if File.directory? yaml
|
139
|
+
yaml_files = yaml if File.exists? yaml
|
140
|
+
end
|
141
|
+
|
142
|
+
unless yaml_files
|
143
|
+
Dust.print_failed "#{yaml} doesn't exist. exiting."
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
yaml_files.each do |file|
|
148
|
+
node = YAML.load_file(file)
|
149
|
+
|
150
|
+
# if the file is empty, just skip it
|
151
|
+
next unless node
|
152
|
+
|
153
|
+
# if there is not hostname field in the yaml file,
|
154
|
+
# treat this node file as a template, and skip to the next one
|
155
|
+
next unless node['hostname']
|
156
|
+
|
157
|
+
# look for the inherits field in the yaml file,
|
158
|
+
# and merge the templates recursively into this node
|
159
|
+
if node['inherits']
|
160
|
+
inherited = {}
|
161
|
+
node.delete('inherits').each do |file|
|
162
|
+
template = YAML.load_file "./nodes/#{file}.yaml"
|
163
|
+
inherited.deep_merge! template
|
164
|
+
end
|
165
|
+
node = inherited.deep_merge node
|
166
|
+
end
|
167
|
+
|
168
|
+
# if more than one hostname is specified, create a node
|
169
|
+
# with the same settings for each hostname
|
170
|
+
node['hostname'].each do |hostname|
|
171
|
+
n = node.clone
|
172
|
+
|
173
|
+
# overwrite hostname with single hostname (in case there are multiple)
|
174
|
+
n['hostname'] = hostname
|
175
|
+
|
176
|
+
# create a new field with the fully qualified domain name
|
177
|
+
n['fqdn'] = hostname
|
178
|
+
n['fqdn'] += '.' + n['domain'] if n['domain']
|
179
|
+
|
180
|
+
# pass command line proxy option
|
181
|
+
n['proxy'] = options[:proxy] if options[:proxy]
|
182
|
+
|
183
|
+
# add this node to the global node array
|
184
|
+
@nodes.push n unless filtered? n
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# checks if this node was filtered out by command line argument
|
190
|
+
# e.g. --filter environment:staging filters out all machines but
|
191
|
+
# those in the environment staging
|
192
|
+
def filtered? node
|
193
|
+
|
194
|
+
# if filter is not specified, instantly return false
|
195
|
+
return false unless options[:filter]
|
196
|
+
|
197
|
+
# remove items if other filter arguments don't match
|
198
|
+
options[:filter].each do |k, v|
|
199
|
+
next unless v # skip empty filters
|
200
|
+
|
201
|
+
# filter if this node doesn't even have the attribute
|
202
|
+
return true unless node[k]
|
203
|
+
|
204
|
+
# allow multiple filters of the same type, divided by ','
|
205
|
+
# e.g. --filter environment:staging,production
|
206
|
+
return true unless v.split(',').include? node[k]
|
207
|
+
end
|
208
|
+
|
209
|
+
# no filter matched, so this host is not filtered.
|
210
|
+
false
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
Deploy.start
|
215
|
+
end
|
data/dust.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "dust/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dust-deploy"
|
7
|
+
s.version = Dust::VERSION
|
8
|
+
s.authors = ["kris kechagia"]
|
9
|
+
s.email = ["kk@rndsec.net"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{small server deployment tool for complex environments}
|
12
|
+
s.description = %q{when puppet and chef suck because you want to be in control and sprinkle just cannot to enough for you}
|
13
|
+
|
14
|
+
s.rubyforge_project = "dust-deploy"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
24
|
+
s.add_runtime_dependency 'net-ssh'
|
25
|
+
s.add_runtime_dependency 'net-scp'
|
26
|
+
s.add_runtime_dependency 'net-sftp'
|
27
|
+
s.add_runtime_dependency 'thor'
|
28
|
+
end
|
data/lib/dust.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dust
|
2
|
+
|
3
|
+
# converts string to kilobytes (rounded)
|
4
|
+
def self.convert_size s
|
5
|
+
i, unit = s.split(' ')
|
6
|
+
|
7
|
+
case unit.downcase
|
8
|
+
when 'kb'
|
9
|
+
return i.to_i
|
10
|
+
when 'mb'
|
11
|
+
return (i.to_f * 1024).to_i
|
12
|
+
when 'gb'
|
13
|
+
return (i.to_f * 1024 * 1024).to_i
|
14
|
+
when 'tb'
|
15
|
+
return (i.to_f * 1024 * 1024 * 1024).to_i
|
16
|
+
else
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Dust
|
2
|
+
# colors for terminal
|
3
|
+
def self.red thick=1; "\033[#{thick};31m"; end
|
4
|
+
def self.green thick=1; "\033[#{thick};32m"; end
|
5
|
+
def self.yellow thick=1; "\033[#{thick};33m"; end
|
6
|
+
def self.blue thick=1; "\033[#{thick};34m"; end
|
7
|
+
def self.pink thick=1; "\033[#{thick};35m"; end
|
8
|
+
def self.turquois thick=1; "\033[#{thick};36m"; end
|
9
|
+
def self.grey thick=1; "\033[#{thick};37m"; end
|
10
|
+
def self.black thick=1; "\033[#{thick};38m"; end
|
11
|
+
def self.none; "\033[0m"; end
|
12
|
+
|
13
|
+
$stdout.sync = true # autoflush
|
14
|
+
|
15
|
+
def self.print_result ret, quiet=false
|
16
|
+
if ret == 0 or ret == true
|
17
|
+
print_ok unless quiet
|
18
|
+
return true
|
19
|
+
else
|
20
|
+
print_failed unless quiet
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.print_ok string="", level=0
|
26
|
+
print_msg "#{string} #{blue}[ ok ]#{none}\n", level
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.print_failed string="", level=0
|
30
|
+
print_msg "#{string} #{red}[ failed ]#{none}\n", level
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.print_warning string="", level=0
|
34
|
+
print_msg "#{string} #{yellow}[ warning ]#{none}\n", level
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.print_hostname hostname, level=0
|
38
|
+
print_msg "\n[ #{blue}#{hostname}#{none} ]\n\n", level
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.print_recipe recipe, level=0
|
42
|
+
print_msg "#{green}|#{recipe}|#{none}\n", level
|
43
|
+
end
|
44
|
+
|
45
|
+
# indent according to level
|
46
|
+
# level 0
|
47
|
+
# - level 1
|
48
|
+
# - level 2
|
49
|
+
def self.print_msg string, level=1
|
50
|
+
if level == 0
|
51
|
+
print string
|
52
|
+
else
|
53
|
+
print ' ' + ' ' * (level - 1) + '- ' + string
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|