management 0.9

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 ADDED
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,86 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ builder (3.2.2)
5
+ celluloid (0.15.2)
6
+ timers (~> 1.1.0)
7
+ coderay (1.1.0)
8
+ diff-lcs (1.2.5)
9
+ excon (0.32.1)
10
+ fakefs (0.5.2)
11
+ ffi (1.9.3)
12
+ fog (1.21.0)
13
+ fog-brightbox
14
+ fog-core (~> 1.21, >= 1.21.1)
15
+ fog-json
16
+ nokogiri (~> 1.5, >= 1.5.11)
17
+ fog-brightbox (0.0.1)
18
+ fog-core
19
+ fog-json
20
+ fog-core (1.21.1)
21
+ builder
22
+ excon (~> 0.32)
23
+ formatador (~> 0.2.0)
24
+ mime-types
25
+ net-scp (~> 1.1)
26
+ net-ssh (>= 2.1.3)
27
+ fog-json (1.0.0)
28
+ multi_json (~> 1.0)
29
+ formatador (0.2.4)
30
+ guard (2.6.1)
31
+ formatador (>= 0.2.4)
32
+ listen (~> 2.7)
33
+ lumberjack (~> 1.0)
34
+ pry (>= 0.9.12)
35
+ thor (>= 0.18.1)
36
+ guard-rspec (4.2.9)
37
+ guard (~> 2.1)
38
+ rspec (>= 2.14, < 4.0)
39
+ listen (2.7.7)
40
+ celluloid (>= 0.15.2)
41
+ rb-fsevent (>= 0.9.3)
42
+ rb-inotify (>= 0.9)
43
+ lumberjack (1.0.6)
44
+ method_source (0.8.2)
45
+ mime-types (2.2)
46
+ mini_portile (0.5.3)
47
+ multi_json (1.9.2)
48
+ net-scp (1.1.2)
49
+ net-ssh (>= 2.6.5)
50
+ net-ssh (2.8.0)
51
+ nokogiri (1.6.1)
52
+ mini_portile (~> 0.5.0)
53
+ pry (0.9.12.6)
54
+ coderay (~> 1.0)
55
+ method_source (~> 0.8)
56
+ slop (~> 3.4)
57
+ rake (10.3.2)
58
+ rb-fsevent (0.9.4)
59
+ rb-inotify (0.9.5)
60
+ ffi (>= 0.5.0)
61
+ rspec (2.14.1)
62
+ rspec-core (~> 2.14.0)
63
+ rspec-expectations (~> 2.14.0)
64
+ rspec-mocks (~> 2.14.0)
65
+ rspec-core (2.14.8)
66
+ rspec-expectations (2.14.5)
67
+ diff-lcs (>= 1.1.3, < 2.0)
68
+ rspec-mocks (2.14.6)
69
+ slop (3.5.0)
70
+ thor (0.19.1)
71
+ timers (1.1.0)
72
+ unf (0.1.4)
73
+ unf_ext
74
+ unf_ext (0.0.6)
75
+
76
+ PLATFORMS
77
+ ruby
78
+
79
+ DEPENDENCIES
80
+ fakefs
81
+ fog
82
+ guard-rspec
83
+ pry
84
+ rake
85
+ rspec
86
+ unf
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch('spec/main_spec.rb') { "spec" }
6
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
7
+ end
data/README.md ADDED
@@ -0,0 +1,193 @@
1
+ ## Management
2
+
3
+ Minimalist EC2 configuration & deployment tool.
4
+
5
+ - Version: **0.9**
6
+
7
+ ![build status](https://travis-ci.org/sdegutis/management.svg?branch=master)
8
+
9
+ #### Usage
10
+
11
+ ```
12
+ $ management
13
+ Usage:
14
+
15
+ create-server <env> <type>
16
+ destroy-server <server>
17
+ list-servers [<env>]
18
+ run-script <server> <script>
19
+ ssh-server <server>
20
+
21
+ -h, --help Display this screen
22
+ -v, --version Show version
23
+
24
+ $ management list-servers
25
+
26
+ $ management create-server staging web
27
+ Created "staging-web-1".
28
+
29
+ $ management list-servers
30
+ Name State IP Private IP
31
+ --------------- ---------- -------------------- --------------------
32
+ staging-db-1 active 107.170.80.230 10.128.198.115
33
+
34
+ $ management run-script staging-web-1 setup-web
35
+ Copying resources/scripts/bootstrap_base.sh -> /home/webapp/bootstrap_base.sh
36
+ Running /home/webapp/bootstrap_base.sh
37
+ [...snip...]
38
+ Copying resources/files/web.conf.erb -> /etc/init/web.conf
39
+ Copying resources/files/nginx.conf -> /etc/init/nginx.conf
40
+ Copying resources/scripts/start_web_server.sh -> /home/webapp/start_web_server.sh
41
+ Running /home/webapp/start_web_server.sh
42
+ [...snip...]
43
+ ```
44
+
45
+ #### Niche
46
+
47
+ The remote server only needs ssh and `tar -xzf` to be available, which
48
+ means it'll work in pretty much any linux server, out-of-the-box.
49
+
50
+ If you only need to provision and manage a handful of servers, this
51
+ project may be right for you.
52
+
53
+ #### Setup
54
+
55
+ Put this in `management_config.yml':
56
+
57
+ ```yaml
58
+ cloud: # NOTE: this just gets passed to Fog::Compute.new
59
+ provider: AWS
60
+ aws_access_key_id: 123
61
+ aws_secret_access_key: 456
62
+ region: New York 1
63
+
64
+ envs:
65
+ - staging
66
+ - production
67
+
68
+ types:
69
+ web: # NOTE: this just gets passed to compute.servers.create
70
+ image_id: ami-1234
71
+ flavor_id: m1.small
72
+ key_name: my-ssh-key-name
73
+ groups: ["web"]
74
+ ssh_key_path: resources/my-ssh-key
75
+
76
+ scripts:
77
+ setup-web:
78
+ - copy: [resources/scripts/bootstrap_base.sh, /home/webapp/bootstrap_base.sh]
79
+ - run: /home/webapp/bootstrap_base.sh
80
+ - copy: [resources/files/web.conf.erb, /etc/init/web.conf, template: true]
81
+ - copy: [resources/files/nginx.conf, /etc/init/nginx.conf]
82
+ - copy: [resources/scripts/start_web_server.sh, /home/webapp/start_web_server.sh]
83
+ - run: /home/webapp/start_web_server.sh
84
+ ```
85
+
86
+ Management doesn't care where any of your files are, with the exception of
87
+ `management_config.yml`, which it expects to be in your project's
88
+ root. Here's the relevant part of the file structure that the above
89
+ sample config assumes:
90
+
91
+ ```
92
+ ./my-project
93
+ |-- management_config.yml
94
+ `-- resources
95
+ |-- files
96
+ | |-- nginx.conf
97
+ | `-- web.conf.erb
98
+ |-- keys
99
+ | |-- id_rsa_digitalocean
100
+ | `-- id_rsa_digitalocean.pub
101
+ `-- scripts
102
+ |-- bootstrap_base.sh
103
+ `-- start_web_server.sh
104
+ ```
105
+
106
+ #### Details
107
+
108
+ Most of how it works should be self-explanatory from the examples
109
+ above. There's just a few things that might not be obvious:
110
+
111
+ 1. Management assumes it's only dealing with servers it created. So it
112
+ assumes that the name will be in the "{env}-{type}-{n}"
113
+ format. But that's really all it assumes.
114
+
115
+ 2. A `copy` line in the `scripts` section will copy all the files from
116
+ the local paths (relative to the project root) to the remote
117
+ *absolute* path, creating directories as needed.
118
+
119
+ 3. If a `copy` line has a third entry of `template: true`, then it
120
+ will be run through ERB. The context will have access to `server`
121
+ representing the Fog server, `cloud` representing the Fog::Compute
122
+ instance, and `configs` representing your configs (YAML). Also,
123
+ each Fog server has two new methods: `env` and `type`. NOTE: if you
124
+ only specified a directory, and it happens to contain `.erb` files,
125
+ they won't be templated.
126
+
127
+ 4. A `run` line in a script will be run on the remote server. The
128
+ paths represent the remote *absolute* paths. It's your
129
+ responsibility to make sure they're executable.
130
+
131
+ 5. Files specified by `run` aren't copied for you automatically; they
132
+ should either already be on the remote server, or you should copy
133
+ them with a `copy` line.
134
+
135
+ 6. The `envs` section is strictly there to catch typos and
136
+ wrongly-ordered arguments at the command line. You can only
137
+ create/destroy/etc servers in a valid environment.
138
+
139
+ 7. The `create-server` command doesn't run any scripts for you, it
140
+ just creates a new server based on the given type.
141
+
142
+ 8. The `scripts` section is admittedly poorly named, since each
143
+ "script" is really an ordered list of files to copy and scripts to
144
+ run remotely. Couldn't think of a better word for it though that
145
+ wasn't too far out there or knee-deep in analogies. I'd love some
146
+ suggestions.
147
+
148
+ 9. A `script` line doesn't have to be a bash scripts, although that's
149
+ the simplest way. It just needs to be something executable. TIP: if
150
+ it's a bash script, it's a good idea to add `set -e` and `set -x`
151
+ to the top of them.
152
+
153
+ #### Example Scripts
154
+
155
+ If you wanted to install Ruby 2 in your setup phase, you might add
156
+ this to one of your scripts
157
+ ([courtesy of Brandon Hilkert](https://github.com/brandonhilkert/fucking_shell_scripts)):
158
+
159
+ ```bash
160
+ sudo apt-get -y install build-essential zlib1g-dev libssl-dev libreadline6-dev libyaml-dev
161
+ cd /tmp
162
+ wget http://ftp.ruby-lang.org/pub/ruby/2.0/ruby-2.0.0-p247.tar.gz
163
+ tar -xzf ruby-2.0.0-p247.tar.gz
164
+ cd ruby-2.0.0-p247
165
+ ./configure --prefix=/usr/local
166
+ make
167
+ sudo make install
168
+ rm -rf /tmp/ruby*
169
+ ```
170
+
171
+ #### License
172
+
173
+ > Released under MIT license.
174
+ >
175
+ > Copyright (c) 2013 Steven Degutis
176
+ >
177
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
178
+ > of this software and associated documentation files (the "Software"), to deal
179
+ > in the Software without restriction, including without limitation the rights
180
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
181
+ > copies of the Software, and to permit persons to whom the Software is
182
+ > furnished to do so, subject to the following conditions:
183
+ >
184
+ > The above copyright notice and this permission notice shall be included in
185
+ > all copies or substantial portions of the Software.
186
+ >
187
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
188
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
189
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
190
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
191
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
192
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
193
+ > THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new(:spec)
3
+ task :default => :spec
data/bin/management ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/management.rb'
4
+ Management::Interpreter.interpret! ARGV.dup
data/lib/ext/fog.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'fog'
2
+ require 'fog/core/ssh'
3
+
4
+
5
+ # Monkey-patch Fog 1.3.1 to stream SSH output
6
+ # (in real time) to stdout.
7
+ class Fog::SSH::Real
8
+ def run(commands)
9
+ commands = [*commands]
10
+ results = []
11
+ begin
12
+ Net::SSH.start(@address, @username, @options) do |ssh|
13
+ commands.each do |command|
14
+ result = Fog::SSH::Result.new(command)
15
+ ssh.open_channel do |ssh_channel|
16
+ ssh_channel.request_pty
17
+ ssh_channel.exec(command) do |channel, success|
18
+ unless success
19
+ raise "Could not execute command: #{command.inspect}"
20
+ end
21
+
22
+ channel.on_data do |ch, data|
23
+ result.stdout << data
24
+ print data
25
+ end
26
+
27
+ channel.on_extended_data do |ch, type, data|
28
+ next unless type == 1
29
+ result.stderr << data
30
+ print data
31
+ end
32
+
33
+ channel.on_request('exit-status') do |ch, data|
34
+ result.status = data.read_long
35
+ end
36
+
37
+ channel.on_request('exit-signal') do |ch, data|
38
+ result.status = 255
39
+ end
40
+ end
41
+ end
42
+ ssh.loop
43
+ results << result
44
+ end
45
+ end
46
+ rescue Net::SSH::HostKeyMismatch => exception
47
+ exception.remember_host!
48
+ sleep 0.2
49
+ retry
50
+ end
51
+ results
52
+ end
53
+ end
54
+
55
+
56
+ require 'fog/compute/models/server'
57
+ # we're assuming the servers were created via boucher or management
58
+ class Fog::Compute::Server
59
+ def env; tags["Env"]; end
60
+ def type; tags["Meal"]; end
61
+ def name; tags["Name"]; end
62
+
63
+ def copy_file(tar_path, remote_tar_path)
64
+ scp(tar_path, remote_tar_path)
65
+ end
66
+
67
+ def extract_tar(remote_tar_path)
68
+ ssh("tar -xzf #{remote_tar_path} -C /")
69
+ end
70
+
71
+ def chown_r(remote_path, chown)
72
+ ssh("chown -R #{chown} #{remote_path}")
73
+ end
74
+ end
@@ -0,0 +1,97 @@
1
+ require 'fog'
2
+ require 'yaml'
3
+
4
+ module Management
5
+
6
+ class Command
7
+
8
+ class << self
9
+
10
+ def all
11
+ @all ||= []
12
+ end
13
+
14
+ def inherited(subclass)
15
+ all << subclass
16
+ end
17
+
18
+ def help_string
19
+ params = instance_method(:call).parameters
20
+
21
+ output = sprintf("%20s ", command_name)
22
+ args = []
23
+
24
+ params.each do |req, name|
25
+ name = "<#{name.to_s.sub('_name', '')}>"
26
+ if req == :opt
27
+ name = "[#{name}]"
28
+ end
29
+ args << name
30
+ end
31
+
32
+ return output + args.join(' ')
33
+ end
34
+
35
+ def command_name
36
+ self.name.split('::').last.
37
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
38
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
39
+ tr("_", "-").
40
+ downcase
41
+ end
42
+
43
+ end
44
+
45
+
46
+ def get_env(name)
47
+ return nil if name.nil?
48
+ config[:envs].include?(name) and name or invalid_selection "Invalid environment: #{name}", config[:envs]
49
+ end
50
+
51
+ def get_type(name)
52
+ config[:types][name.to_sym] or invalid_selection "Invalid type: #{name}", config[:types].map(&:first)
53
+ end
54
+
55
+ def get_script(name)
56
+ config[:scripts][name.to_sym] or invalid_selection "Invalid script: #{name}", config[:scripts].map(&:first)
57
+ end
58
+
59
+ def get_server(name)
60
+ servers = cloud.servers
61
+ servers.find{|server| server.name == name} or invalid_selection "Invalid server: #{name}", servers.map(&:name)
62
+ end
63
+
64
+ def config
65
+ @config ||= symbolize_keys!(raw_yaml)
66
+ end
67
+
68
+ def cloud
69
+ @cloud ||= Fog::Compute.new(config[:cloud])
70
+ end
71
+
72
+
73
+ private
74
+
75
+ def raw_yaml
76
+ YAML.load(File.read("management_config.yml"))
77
+ end
78
+
79
+ def invalid_selection(str, selection)
80
+ abort "#{str}\nValid choices:" + (["\n"] + selection).join("\n - ")
81
+ end
82
+
83
+ def symbolize_keys! h
84
+ case h
85
+ when Hash
86
+ pairs = h.map { |k, v| [k.respond_to?(:to_sym) ? k.to_sym : k, symbolize_keys!(v)] }
87
+ return Hash[pairs]
88
+ when Array
89
+ return h.map{ |e| symbolize_keys!(e) }
90
+ else
91
+ return h
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,50 @@
1
+ require_relative '../command'
2
+
3
+ module Management
4
+
5
+ class CreateServer < Management::Command
6
+
7
+ def call(env_name, type_name)
8
+ env = get_env(env_name)
9
+ type = get_type(type_name)
10
+
11
+ servers = cloud.servers
12
+ name = make_unique_server_name(env_name, type_name, servers)
13
+
14
+ puts "Creating \"#{name}\"..."
15
+
16
+ cloud.servers.create(image_id: type[:image_id],
17
+ flavor_id: type[:flavor_id],
18
+ groups: type[:groups],
19
+ key_name: type[:key_name],
20
+ tags: {
21
+ "Creator" => current_user,
22
+ "CreatedAt" => Time.new.strftime("%Y%m%d%H%M%S"),
23
+ "Name" => name,
24
+ "Env" => env_name,
25
+ "Meal" => type_name,
26
+ })
27
+
28
+ puts "Done."
29
+ end
30
+
31
+ def current_user
32
+ `git config user.name`.strip
33
+ rescue
34
+ "unknown"
35
+ end
36
+
37
+ def make_unique_server_name(env_name, type_name, servers)
38
+ (1..Float::INFINITY).each do |i|
39
+ name = "#{env_name}-#{type_name}-#{i}"
40
+ if servers.find{|s|s.name == name}
41
+ i += 1
42
+ else
43
+ return name
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../command'
2
+
3
+ module Management
4
+
5
+ class DestroyServer < Management::Command
6
+
7
+ def call(server_name)
8
+ server = get_server(server_name)
9
+
10
+ print "Are you sure you want to do this? Type 'Yes' to continue, or anything else to abort: "
11
+ answer = $stdin.gets.chomp
12
+
13
+ if answer == 'Yes'
14
+ server.destroy
15
+ puts "Destroyed."
16
+ else
17
+ puts "Aborted."
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../command'
2
+
3
+ module Management
4
+
5
+ class ListServers < Management::Command
6
+
7
+ def call(env_name = nil)
8
+ env = get_env(env_name)
9
+
10
+ cols = [
11
+ {size: 20, title: "Name", field: :name },
12
+ {size: 10, title: "State", field: :state },
13
+ {size: 20, title: "IP", field: :public_ip_address },
14
+ {size: 20, title: "Private IP", field: :private_ip_address },
15
+ {size: 10, title: "Size", field: :flavor_id },
16
+ {size: 15, title: "Env", field: :env },
17
+ {size: 15, title: "Type", field: :type },
18
+ {size: 11, title: "EC2 ID", field: :id },
19
+ ]
20
+
21
+ format = cols.map{|c| "%-#{c[:size]}s"}.join(" ") + "\n"
22
+
23
+ send :printf, *([format].concat(cols.map{|c|c[:title]}))
24
+ send :printf, *([format].concat(cols.map{|c|'-' * c[:size]}))
25
+
26
+ servers = cloud.servers.sort_by(&:name)
27
+
28
+ servers.each do |server|
29
+ next if env_name && server.env != env_name
30
+ next if server.state == 'terminated'
31
+
32
+ send :printf, *([format].concat(cols.map{|c|server.send(c[:field])}))
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,123 @@
1
+ require_relative '../command'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+ require 'erb'
5
+ require 'shellwords'
6
+
7
+ module Management
8
+
9
+ class RunScript < Management::Command
10
+
11
+ def call(server_name, script_name)
12
+ server = get_server(server_name)
13
+ script = get_script(script_name)
14
+
15
+ server.private_key_path = config[:types][server.type.to_sym][:ssh_key_path]
16
+
17
+ missing = missing_local_files(script)
18
+ abort "The following files are missing:" + (["\n"] + missing).join("\n - ") if !missing.empty?
19
+
20
+ script.each do |tuple|
21
+ type, data = *tuple.first
22
+
23
+ case type.to_sym
24
+ when :copy
25
+ copy_file(server, *data)
26
+ when :run
27
+ run_command(server, data)
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+
34
+ def copy_file(server, local_path, remote_path, opts = nil)
35
+ should_template = opts && opts[:template]
36
+ custom_chown = opts && opts[:chown]
37
+
38
+ puts "Copying #{local_path} -> #{remote_path}"
39
+
40
+ Dir.mktmpdir('management-file-dir') do |file_tmpdir|
41
+
42
+ # copy to the fake "remote" path locally
43
+ remote_looking_path = File.join(file_tmpdir, remote_path)
44
+ FileUtils.mkdir_p File.dirname(remote_looking_path)
45
+ FileUtils.cp_r local_path, remote_looking_path, preserve: true
46
+
47
+ # overwrite the fake "remote" file with its own templated contents if necessary
48
+ if should_template
49
+ new_contents = ERB.new(File.read(remote_looking_path)).result(binding)
50
+ File.write(remote_looking_path, new_contents)
51
+ end
52
+
53
+ Dir.mktmpdir('management-tar-dir') do |tar_tmpdir|
54
+
55
+ # zip this file up, starting from its absolute path
56
+ local_tar_path = File.join(tar_tmpdir, "__management__.tar.gz")
57
+ zip_relevant_files(file_tmpdir, local_tar_path)
58
+
59
+ # copy tar file to remote and extract
60
+ remote_tar_path = "/tmp/__management__.tar.gz"
61
+ server.copy_file(local_tar_path, remote_tar_path)
62
+ server.extract_tar(remote_tar_path)
63
+ server.chown_r(remote_path, custom_chown) if custom_chown
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ def run_command(server, cmd)
72
+ puts "Running #{cmd}"
73
+
74
+ result = server.ssh("#{cmd}").first
75
+
76
+ if result.respond_to?(:status)
77
+ puts
78
+ puts "---------------------------"
79
+ if result.status == 0
80
+ puts "Success!"
81
+ else
82
+ puts "Failed. Exit code: #{result.status}"
83
+ end
84
+ end
85
+ end
86
+
87
+ def missing_local_files(script)
88
+ script.find_all do |tuple|
89
+ type, data = *tuple.first
90
+ if type == :copy
91
+ local, remote = *data
92
+ ! File.exists?(local)
93
+ end
94
+ end.map do |tuple|
95
+ type, data = *tuple.first
96
+ local, remote = *data
97
+ local
98
+ end
99
+ end
100
+
101
+ def relevant_files(at_dir)
102
+ abort unless at_dir.start_with? "/"
103
+
104
+ Dir[File.join(at_dir, "**/*")].select do |path|
105
+ File.file?(path) || (File.directory?(path) && Dir.entries(path) == [".", ".."])
106
+ end.map do |path|
107
+ path.slice! at_dir.end_with?("/") ? at_dir : "#{at_dir}/"
108
+ "./#{path}"
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def zip_relevant_files(in_dir, out_file)
115
+ Dir.chdir(in_dir) do
116
+ file_list = Shellwords.join(relevant_files(in_dir))
117
+ system("tar -czf #{out_file} #{file_list}")
118
+ end
119
+ end
120
+
121
+ end
122
+
123
+ end
@@ -0,0 +1,23 @@
1
+ require_relative '../command'
2
+
3
+ module Management
4
+
5
+ class SshServer < Management::Command
6
+
7
+ def call(server_name)
8
+ server = get_server(server_name)
9
+
10
+ type = config[:types][server.type.to_sym]
11
+ ssh_key_path = type[:ssh_key_path]
12
+ run "chmod 0600 #{ssh_key_path}"
13
+ run "ssh -i #{ssh_key_path} #{config[:root_user]}@#{server.public_ip_address}"
14
+ end
15
+
16
+ def run(cmd)
17
+ puts "Running: #{cmd}"
18
+ system cmd
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '../command'
2
+
3
+ module Management
4
+
5
+ class StopServer < Management::Command
6
+
7
+ def call(server_name)
8
+ server = get_server(server_name)
9
+ server.stop
10
+ puts "Stopped #{server_name}."
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,51 @@
1
+ require 'optparse'
2
+
3
+ module Management
4
+
5
+ class Interpreter
6
+
7
+ class << self
8
+
9
+ def interpret!(input)
10
+ commands = Management::Command.all
11
+
12
+ parser = OptionParser.new do |opts|
13
+ opts.banner = "Usage:"
14
+ opts.separator('')
15
+ commands.each { |cmd| opts.separator cmd.help_string }
16
+ opts.separator('')
17
+ opts.on('-h', '--help', 'Display this screen') { puts opts; exit }
18
+ opts.on('-v', '--version', 'Show version') { puts Management::VERSION; exit }
19
+ end
20
+
21
+ abort parser.help if input.empty?
22
+
23
+ args = parser.parse(input)
24
+ task = args.shift
25
+ ARGV.clear
26
+
27
+ if chosen_command = commands.find{|c|c.command_name == task}
28
+ all_args = chosen_command.instance_method(:call).parameters
29
+ req_args = all_args.map(&:first).take_while{|p| p == :req}
30
+
31
+ case
32
+ when args.count < req_args.count
33
+ puts "Error: not enough arguments"
34
+ abort parser.help
35
+ when args.count > all_args.count
36
+ puts "Error: too many arguments"
37
+ abort parser.help
38
+ else
39
+ chosen_command.new.call(*args)
40
+ end
41
+ else
42
+ puts "Error: unknown task \"#{task}\""
43
+ abort parser.help
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,3 @@
1
+ module Management
2
+ VERSION = "0.9"
3
+ end
data/lib/management.rb ADDED
@@ -0,0 +1,10 @@
1
+ require_relative 'ext/fog'
2
+
3
+ require_relative 'management/version'
4
+ require_relative 'management/interpreter'
5
+ require_relative 'management/commands/create_server'
6
+ require_relative 'management/commands/list_servers'
7
+ require_relative 'management/commands/destroy_server'
8
+ require_relative 'management/commands/run_script'
9
+ require_relative 'management/commands/ssh_server'
10
+ require_relative 'management/commands/stop_server'
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "management/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'management'
7
+ s.version = Management::VERSION
8
+ s.email = 'steven@cleancoders.com'
9
+ s.authors = ["Steven Degutis"]
10
+ s.homepage = 'https://github.com/sdegutis/management'
11
+ s.license = 'MIT'
12
+ s.summary = "Minimalist EC2 management & deployment tool."
13
+ s.description = "Write your deployment using just shell scripts."
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- spec/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency 'fog'
20
+ s.add_dependency 'unf' # just to shut up the warnings
21
+
22
+ s.add_development_dependency 'rake'
23
+ s.add_development_dependency 'pry'
24
+ s.add_development_dependency 'fakefs'
25
+ s.add_development_dependency 'rspec'
26
+ s.add_development_dependency 'guard-rspec'
27
+ end
data/spec/main_spec.rb ADDED
@@ -0,0 +1,256 @@
1
+ require_relative '../lib/management'
2
+ require 'fakefs/spec_helpers'
3
+ require 'stringio'
4
+ require 'pry'
5
+ require 'etc'
6
+
7
+
8
+ SampleConfig = <<EOC
9
+ cloud:
10
+ provider: AWS
11
+ aws_access_key_id: 123
12
+ aws_secret_access_key: 456
13
+ region: New York 1
14
+
15
+ envs:
16
+ - staging
17
+ - production
18
+
19
+ types:
20
+ web:
21
+ image_id: ami-1234
22
+ flavor_id: m1.small
23
+ key_name: my-ssh-key-name
24
+ groups: ["web"]
25
+ ssh_key_path: resources/my-ssh-key
26
+
27
+ scripts:
28
+ testing:
29
+ - copy: [resources/testing.sh, /home/web/testing.sh]
30
+ - copy: [resources/web.conf.erb, /etc/init/web.conf, template: true]
31
+ - run: /home/web/testing.sh
32
+ EOC
33
+
34
+
35
+ def with_stdin(s) old = $stdin; $stdin = StringIO.new(s); yield; $stdin = old end
36
+ def without_stdout old = $stdout; $stdout = StringIO.new; yield; $stdout = old end
37
+ def without_stderr old = $stderr; $stderr = StringIO.new; yield; $stderr = old end
38
+
39
+ describe 'management' do
40
+
41
+ before { subject.stub(:raw_yaml).and_return(YAML.load(SampleConfig)) }
42
+
43
+ describe Management::Command do
44
+
45
+ describe "safely getting config values" do
46
+
47
+ it "can get env" do
48
+ expect { without_stderr { subject.get_env("FAKE") } }.to raise_error SystemExit
49
+ expect { without_stderr { subject.get_env("staging") } }.to_not raise_error
50
+ end
51
+
52
+ it "can get type" do
53
+ expect { without_stderr { subject.get_type("FAKE") } }.to raise_error SystemExit
54
+ expect { without_stderr { subject.get_type("web") } }.to_not raise_error
55
+ end
56
+
57
+ it "can get script" do
58
+ expect { without_stderr { subject.get_script("FAKE") } }.to raise_error SystemExit
59
+ expect { without_stderr { subject.get_script("testing") } }.to_not raise_error
60
+ end
61
+
62
+ end
63
+
64
+
65
+ end
66
+
67
+ describe Management::RunScript do
68
+
69
+ include FakeFS::SpecHelpers
70
+
71
+ describe "finding relevant files to zip" do
72
+
73
+ it "finds all files in the tree" do
74
+ FileUtils.mkdir_p("/foo/bar/baz")
75
+ File.write("/foo/bar/baz/quux", "woot")
76
+ File.write("/foo/bar/baz/zap", "wat")
77
+ subject.relevant_files("/").should == ["./foo/bar/baz/quux", "./foo/bar/baz/zap"]
78
+ end
79
+
80
+ it "finds empty leaf directories in the tree" do
81
+ FileUtils.mkdir_p("/foo/bar/baz")
82
+ File.write("/foo/bar/baz/quux", "woot")
83
+ FileUtils.mkdir_p("/foo/bar/baz/zap")
84
+ subject.relevant_files("/").should == ["./foo/bar/baz/quux", "./foo/bar/baz/zap"]
85
+ end
86
+
87
+ it "returns dot files" do
88
+ FileUtils.mkdir_p("/foo/bar/baz")
89
+ File.write("/foo/bar/baz/.quux", "woot")
90
+ FileUtils.mkdir_p("/foo/bar/baz/.zap")
91
+ subject.relevant_files("/").should == ["./foo/bar/baz/.quux", "./foo/bar/baz/.zap"]
92
+ end
93
+
94
+ it "returns relative filenames" do
95
+ FileUtils.mkdir_p("/foo/bar/baz")
96
+ File.write("/foo/bar/baz/quux", "woot")
97
+ FileUtils.mkdir_p("/foo/bar/baz/zap")
98
+ subject.relevant_files("/foo/bar").should == ["./baz/quux", "./baz/zap"]
99
+ end
100
+
101
+ it "returns relative filenames, even when you add a trailing slash" do
102
+ FileUtils.mkdir_p("/foo/bar/baz")
103
+ File.write("/foo/bar/baz/quux", "woot")
104
+ FileUtils.mkdir_p("/foo/bar/baz/zap")
105
+ subject.relevant_files("/foo/bar/").should == ["./baz/quux", "./baz/zap"]
106
+ end
107
+
108
+ it "requires an absolute path" do
109
+ FileUtils.mkdir_p("/foo")
110
+ expect{ subject.relevant_files("foo") }.to raise_error SystemExit
111
+ end
112
+
113
+ end
114
+
115
+ describe "copying files over" do
116
+
117
+ let(:server) { Object.new }
118
+
119
+ before(:each) do
120
+
121
+ # just copy_r the given directory's contents into a new temp dir
122
+ # and put the filename of that new temp dir into out_file
123
+ subject.define_singleton_method(:zip_relevant_files) do |in_dir, out_file|
124
+ zip_dir = Dir.mktmpdir("fake-local-zip-dir")
125
+ FileUtils.cp_r Dir[File.join(in_dir, "*")], zip_dir
126
+ File.write(out_file, zip_dir)
127
+ end
128
+
129
+ server.define_singleton_method(:name) { "server-1" }
130
+ server.define_singleton_method(:env) { "staging" }
131
+
132
+ # local just contains the name of a dir containing all the files
133
+ server.define_singleton_method(:copy_file) do |local, remote|
134
+ # copying "local" zip file to "remote" zip file
135
+ fake_remote = File.join("/fake-remote-dir", remote)
136
+ FileUtils.cp local, fake_remote
137
+ end
138
+
139
+ # just cp_r the files under fake-zip-dir into /fake-remote-dir
140
+ server.define_singleton_method(:extract_tar) do |remote|
141
+ tar_dir = File.read(File.join("/fake-remote-dir", remote))
142
+ FileUtils.cp_r(File.join(tar_dir, "*"), "/fake-remote-dir")
143
+ end
144
+
145
+ server.define_singleton_method(:chown_r) do |remote, chowner|
146
+ user, group = chowner.split(":")
147
+ FileUtils.chown_R(user, group, File.join("/fake-remote-dir", remote))
148
+ end
149
+
150
+ end
151
+
152
+ it "copies file contents into their remote paths" do
153
+ File.write("foo", "the contents of foo")
154
+ without_stdout { subject.copy_file(server, "foo", "/remote/foo") }
155
+ File.read("/fake-remote-dir/remote/foo").should == "the contents of foo"
156
+ end
157
+
158
+ it "templates files correctly" do
159
+ File.write("foo", "the contents of <%= server.env %>")
160
+ without_stdout { subject.copy_file(server, "foo", "/remote/foo", template: true) }
161
+ File.read("/fake-remote-dir/remote/foo").should == "the contents of staging"
162
+ end
163
+
164
+ it "chowns files correctly when specified" do
165
+ user = Etc.passwd.name
166
+ group = Etc.group.name
167
+
168
+ File.write("foo", "hello world")
169
+ without_stdout { subject.copy_file(server, "foo", "/remote/foo", chown: "#{user}:#{group}") }
170
+
171
+ stats = File.stat("/fake-remote-dir/remote/foo")
172
+ Etc.getpwuid(stats.uid).name.should == user
173
+ Etc.getgrgid(stats.gid).name.should == group
174
+ end
175
+
176
+ it "doesn't chown anything unless specified" do
177
+ File.write("foo", "hello world")
178
+ without_stdout { subject.copy_file(server, "foo", "/remote/foo") }
179
+
180
+ stats = File.stat("/fake-remote-dir/remote/foo")
181
+ Etc.getpwuid(stats.uid).name.should == `id -un`.chomp
182
+ Etc.getgrgid(stats.gid).name.should == `id -gn`.chomp
183
+ end
184
+
185
+ it "fails if multiple local paths don't exist" do
186
+ script = subject.get_script("testing")
187
+ list = subject.missing_local_files(script)
188
+ list.should == ["resources/testing.sh", "resources/web.conf.erb"]
189
+ end
190
+
191
+ it "fails if a single local path doesn't exist" do
192
+ FileUtils.mkdir_p "resources"
193
+ File.write "resources/testing.sh", "hello world"
194
+ script = subject.get_script("testing")
195
+ list = subject.missing_local_files(script)
196
+ list.should == ["resources/web.conf.erb"]
197
+ end
198
+
199
+ end
200
+
201
+ end
202
+
203
+ describe Management::CreateServer do
204
+
205
+ it "uses unique names for servers" do
206
+ fake_server = Struct.new(:name)
207
+ servers = [fake_server.new('staging-web-1'),
208
+ fake_server.new('production-web-1'),
209
+ fake_server.new('staging-web-2')]
210
+
211
+ subject.make_unique_server_name("staging", "web", []).should == "staging-web-1"
212
+ subject.make_unique_server_name("staging", "web", servers).should == "staging-web-3"
213
+ subject.make_unique_server_name("production", "web", servers).should == "production-web-2"
214
+ end
215
+
216
+ end
217
+
218
+ describe Management::DestroyServer do
219
+
220
+ let(:server) { Object.new }
221
+ before { subject.stub(:get_server).with("server-1").and_return(server) }
222
+
223
+ it "destroys the given server if you type 'Yes' verbatim" do
224
+ server.should_receive(:destroy).once
225
+ with_stdin("Yes\n") { without_stdout { subject.call("server-1") } }
226
+ end
227
+
228
+ it "does not destroy the given server if you don't type 'Yes' verbatim" do
229
+ server.should_not_receive(:destroy)
230
+ without_stdout do
231
+ with_stdin("yes\n") { subject.call("server-1") }
232
+ with_stdin("Y\n") { subject.call("server-1") }
233
+ with_stdin("y\n") { subject.call("server-1") }
234
+ with_stdin("yep\n") { subject.call("server-1") }
235
+ with_stdin("\n") { subject.call("server-1") }
236
+ with_stdin("YES\n") { subject.call("server-1") }
237
+ with_stdin("Yes.\n") { subject.call("server-1") }
238
+ end
239
+ end
240
+
241
+ end
242
+
243
+ describe Management::StopServer do
244
+
245
+ let(:server) { Object.new }
246
+ before { subject.stub(:get_server).with("server-1").and_return(server) }
247
+
248
+ it "stops the given server" do
249
+ server.should_not_receive(:destroy)
250
+ server.should_receive(:stop).once
251
+ without_stdout { subject.call("server-1") }
252
+ end
253
+
254
+ end
255
+
256
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: management
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.9'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steven Degutis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-06-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fog
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
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: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: unf
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
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: fakefs
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rspec
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: guard-rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Write your deployment using just shell scripts.
127
+ email: steven@cleancoders.com
128
+ executables:
129
+ - management
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .travis.yml
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - Guardfile
138
+ - README.md
139
+ - Rakefile
140
+ - bin/management
141
+ - lib/ext/fog.rb
142
+ - lib/management.rb
143
+ - lib/management/command.rb
144
+ - lib/management/commands/create_server.rb
145
+ - lib/management/commands/destroy_server.rb
146
+ - lib/management/commands/list_servers.rb
147
+ - lib/management/commands/run_script.rb
148
+ - lib/management/commands/ssh_server.rb
149
+ - lib/management/commands/stop_server.rb
150
+ - lib/management/interpreter.rb
151
+ - lib/management/version.rb
152
+ - management.gemspec
153
+ - spec/main_spec.rb
154
+ homepage: https://github.com/sdegutis/management
155
+ licenses:
156
+ - MIT
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ none: false
163
+ requirements:
164
+ - - ! '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ none: false
169
+ requirements:
170
+ - - ! '>='
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubyforge_project:
175
+ rubygems_version: 1.8.23.2
176
+ signing_key:
177
+ specification_version: 3
178
+ summary: Minimalist EC2 management & deployment tool.
179
+ test_files:
180
+ - spec/main_spec.rb