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.
@@ -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