dust-deploy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ TODO
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in dust.gemspec
4
+ gemspec
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'dust/print_status'
2
+ require 'dust/convert_size'
3
+ require 'dust/server'
4
+
5
+ module Dust
6
+ end
@@ -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