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 +34 -0
- data/.travis.yml +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +86 -0
- data/Guardfile +7 -0
- data/README.md +193 -0
- data/Rakefile +3 -0
- data/bin/management +4 -0
- data/lib/ext/fog.rb +74 -0
- data/lib/management/command.rb +97 -0
- data/lib/management/commands/create_server.rb +50 -0
- data/lib/management/commands/destroy_server.rb +23 -0
- data/lib/management/commands/list_servers.rb +38 -0
- data/lib/management/commands/run_script.rb +123 -0
- data/lib/management/commands/ssh_server.rb +23 -0
- data/lib/management/commands/stop_server.rb +15 -0
- data/lib/management/interpreter.rb +51 -0
- data/lib/management/version.rb +3 -0
- data/lib/management.rb +10 -0
- data/management.gemspec +27 -0
- data/spec/main_spec.rb +256 -0
- metadata +180 -0
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
data/Gemfile
ADDED
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
data/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
## Management
|
|
2
|
+
|
|
3
|
+
Minimalist EC2 configuration & deployment tool.
|
|
4
|
+
|
|
5
|
+
- Version: **0.9**
|
|
6
|
+
|
|
7
|
+

|
|
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
data/bin/management
ADDED
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,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
|
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'
|
data/management.gemspec
ADDED
|
@@ -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
|