dust-deploy 0.1.0
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 +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
|