stackadmin 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +119 -0
- data/README.md +2 -0
- data/Rakefile +37 -0
- data/lib/stackadmin/audit.rb +145 -0
- data/lib/stackadmin/base.rb +42 -0
- data/lib/stackadmin/exceptions.rb +16 -0
- data/lib/stackadmin/net-ssh.rb +49 -0
- data/lib/stackadmin/patch.rb +232 -0
- data/lib/stackadmin/version.rb +5 -0
- data/lib/stackadmin/yaml.rb +32 -0
- data/lib/stackadmin.rb +7 -0
- data/test/Vagrantfile +14 -0
- data/test/stackadmin_test.rb +325 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9356c67e44d2b41f466700e39b383e6ddf8599d9
|
4
|
+
data.tar.gz: 90080f91deaa13008ee34454051567d44c1ee291
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7cdf3b9b5f4f8157f00c5d3826ea8643fdae871c4105004e0ae11ec8910b14c1b4a448f1b1c17fdeb61a5c60b85c70837f27d082b6dca0416d7d85c836abb058
|
7
|
+
data.tar.gz: 4925bc36f4ab907e38ee5fc41b4aa2d553fe1b615685d7d26040ffa2e77e2b352bde2bd1604b7f887abf738e5d07e7b28473103e78d866aecfb3c27ec9143ba2
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
test/.vagrant
|
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'net-ssh'
|
4
|
+
gem 'json'
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem 'minitest'
|
8
|
+
gem 'minitest-reporters'
|
9
|
+
gem 'vagrant', git: 'https://github.com/mitchellh/vagrant.git'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :plugins do
|
13
|
+
gem 'vagrant-s3auth'
|
14
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
GIT
|
2
|
+
remote: https://github.com/mitchellh/vagrant.git
|
3
|
+
revision: b2c722ef54e57c9de5cdbe712b27e52faacec5da
|
4
|
+
specs:
|
5
|
+
vagrant (1.7.2)
|
6
|
+
bundler (>= 1.5.2, < 1.8.0)
|
7
|
+
childprocess (~> 0.5.0)
|
8
|
+
erubis (~> 2.7.0)
|
9
|
+
hashicorp-checkpoint (~> 0.1.1)
|
10
|
+
i18n (~> 0.6.0)
|
11
|
+
listen (~> 2.8.0)
|
12
|
+
log4r (~> 1.1.9, < 1.1.11)
|
13
|
+
net-scp (~> 1.1.0)
|
14
|
+
net-sftp (~> 2.1)
|
15
|
+
net-ssh (>= 2.6.6, < 2.10.0)
|
16
|
+
nokogiri (= 1.6.3.1)
|
17
|
+
rb-kqueue (~> 0.2.0)
|
18
|
+
rest-client (>= 1.6.0, < 2.0)
|
19
|
+
wdm (~> 0.1.0)
|
20
|
+
winrm (~> 1.3.0)
|
21
|
+
winrm-fs (~> 0.1.0)
|
22
|
+
|
23
|
+
GEM
|
24
|
+
remote: https://rubygems.org/
|
25
|
+
specs:
|
26
|
+
ansi (1.5.0)
|
27
|
+
aws-sdk (1.59.1)
|
28
|
+
aws-sdk-v1 (= 1.59.1)
|
29
|
+
aws-sdk-v1 (1.59.1)
|
30
|
+
json (~> 1.4)
|
31
|
+
nokogiri (>= 1.4.4)
|
32
|
+
builder (3.2.2)
|
33
|
+
celluloid (0.16.0)
|
34
|
+
timers (~> 4.0.0)
|
35
|
+
childprocess (0.5.5)
|
36
|
+
ffi (~> 1.0, >= 1.0.11)
|
37
|
+
erubis (2.7.0)
|
38
|
+
ffi (1.9.6)
|
39
|
+
gssapi (1.2.0)
|
40
|
+
ffi (>= 1.0.1)
|
41
|
+
gyoku (1.2.2)
|
42
|
+
builder (>= 2.1.2)
|
43
|
+
hashicorp-checkpoint (0.1.4)
|
44
|
+
hashie (3.4.0)
|
45
|
+
hitimes (1.2.2)
|
46
|
+
httpclient (2.6.0.1)
|
47
|
+
i18n (0.6.11)
|
48
|
+
json (1.8.2)
|
49
|
+
listen (2.8.5)
|
50
|
+
celluloid (>= 0.15.2)
|
51
|
+
rb-fsevent (>= 0.9.3)
|
52
|
+
rb-inotify (>= 0.9)
|
53
|
+
little-plugger (1.1.3)
|
54
|
+
log4r (1.1.10)
|
55
|
+
logging (1.8.2)
|
56
|
+
little-plugger (>= 1.1.3)
|
57
|
+
multi_json (>= 1.8.4)
|
58
|
+
mime-types (2.4.3)
|
59
|
+
mini_portile (0.6.0)
|
60
|
+
minitest (4.3.2)
|
61
|
+
minitest-reporters (0.14.24)
|
62
|
+
ansi
|
63
|
+
builder
|
64
|
+
minitest (>= 2.12, < 5.0)
|
65
|
+
powerbar
|
66
|
+
multi_json (1.10.1)
|
67
|
+
net-scp (1.1.2)
|
68
|
+
net-ssh (>= 2.6.5)
|
69
|
+
net-sftp (2.1.2)
|
70
|
+
net-ssh (>= 2.6.5)
|
71
|
+
net-ssh (2.9.2)
|
72
|
+
netrc (0.10.2)
|
73
|
+
nokogiri (1.6.3.1)
|
74
|
+
mini_portile (= 0.6.0)
|
75
|
+
nori (2.4.0)
|
76
|
+
powerbar (1.0.12)
|
77
|
+
ansi (~> 1.5.0)
|
78
|
+
hashie (>= 1.1.0)
|
79
|
+
rb-fsevent (0.9.4)
|
80
|
+
rb-inotify (0.9.5)
|
81
|
+
ffi (>= 0.5.0)
|
82
|
+
rb-kqueue (0.2.3)
|
83
|
+
ffi (>= 0.5.0)
|
84
|
+
rest-client (1.7.2)
|
85
|
+
mime-types (>= 1.16, < 3.0)
|
86
|
+
netrc (~> 0.7)
|
87
|
+
rubyntlm (0.4.0)
|
88
|
+
rubyzip (1.1.7)
|
89
|
+
timers (4.0.1)
|
90
|
+
hitimes
|
91
|
+
uuidtools (2.1.5)
|
92
|
+
vagrant-s3auth (1.0.2)
|
93
|
+
aws-sdk (~> 1.59.1)
|
94
|
+
wdm (0.1.0)
|
95
|
+
winrm (1.3.0)
|
96
|
+
builder (>= 2.1.2)
|
97
|
+
gssapi (~> 1.2)
|
98
|
+
gyoku (~> 1.0)
|
99
|
+
httpclient (~> 2.2, >= 2.2.0.2)
|
100
|
+
logging (~> 1.6, >= 1.6.1)
|
101
|
+
nori (~> 2.0)
|
102
|
+
rubyntlm (~> 0.4.0)
|
103
|
+
uuidtools (~> 2.1.2)
|
104
|
+
winrm-fs (0.1.0)
|
105
|
+
erubis (~> 2.7)
|
106
|
+
logging (~> 1.6, >= 1.6.1)
|
107
|
+
rubyzip (~> 1.1)
|
108
|
+
winrm (~> 1.3.0)
|
109
|
+
|
110
|
+
PLATFORMS
|
111
|
+
ruby
|
112
|
+
|
113
|
+
DEPENDENCIES
|
114
|
+
json
|
115
|
+
minitest
|
116
|
+
minitest-reporters
|
117
|
+
net-ssh
|
118
|
+
vagrant!
|
119
|
+
vagrant-s3auth
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Encoding: utf-8
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
task :default => 'test'
|
5
|
+
|
6
|
+
namespace :test do
|
7
|
+
desc 'Spin up test env'
|
8
|
+
task :start_env do
|
9
|
+
Dir.chdir('test') { system('bundle exec vagrant up') }
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'Setup test env'
|
13
|
+
task :setup, :pub_key_location do |t, args|
|
14
|
+
pub_key_location = args[:pub_key_location] || File.expand_path('~/.ssh/keys/id_rsa.pub')
|
15
|
+
pub_key = File.open(pub_key_location, 'r').read
|
16
|
+
Dir.chdir('test') { system("vagrant ssh --command 'echo \"#{pub_key}\" | sudo tee -a /home/stackato/.ssh/authorized_keys' > /dev/null") }
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Destroy test env'
|
20
|
+
task :destroy_env do
|
21
|
+
Dir.chdir('test') { system('bundle exec vagrant destroy --force') }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::TestTask.new('test:run') do |t|
|
26
|
+
t.test_files = FileList['test/*_test.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Run all test tasks'
|
30
|
+
task :test do
|
31
|
+
%w( start_env setup run destroy_env ).each do |j|
|
32
|
+
job = "test:#{j}"
|
33
|
+
puts "\e[35mRunning #{job}...\e[0m"
|
34
|
+
Rake::Task[job].invoke
|
35
|
+
puts
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'exceptions'
|
5
|
+
require_relative 'net-ssh'
|
6
|
+
|
7
|
+
class Stackadmin
|
8
|
+
private
|
9
|
+
|
10
|
+
def audit(target = @id)
|
11
|
+
@id = target
|
12
|
+
log "Auditing #{@id}"
|
13
|
+
|
14
|
+
begin
|
15
|
+
Net::SSH.start(target, 'stackato', port: @port) do |ssh|
|
16
|
+
@version = parse_version(ssh)
|
17
|
+
@license = parse_license(ssh)
|
18
|
+
@nodes = map_nodes(ssh)
|
19
|
+
parse_patch_status(ssh)
|
20
|
+
@fresh = true
|
21
|
+
end
|
22
|
+
rescue Net::SSH::Exception => e
|
23
|
+
log "#{@id} audit failed! #{e}"
|
24
|
+
@fresh = false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_version(ssh)
|
29
|
+
cmd = ssh.exec_sc!('kato info | grep Version')
|
30
|
+
ver = cmd[:stdout].split.last
|
31
|
+
log "Retrieved version: #{ver}"
|
32
|
+
|
33
|
+
ver.gsub!(/v/, '') if ver =~ /^v\d.*$/
|
34
|
+
log "Parsed as version: #{ver}"
|
35
|
+
|
36
|
+
ver
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_license(ssh)
|
40
|
+
license = Hash.new
|
41
|
+
|
42
|
+
ssh.exec_sc!('kato config get cluster license')[:stdout].split("\n").each do |line|
|
43
|
+
line = line.split(': ')
|
44
|
+
license[line[0]] = line[1]
|
45
|
+
end
|
46
|
+
|
47
|
+
license
|
48
|
+
end
|
49
|
+
|
50
|
+
def map_nodes(ssh)
|
51
|
+
cluster = Hash.new
|
52
|
+
|
53
|
+
ssh.exec_sc!('kato node list')[:stdout].split("\n").each do |node|
|
54
|
+
n = node.split
|
55
|
+
cluster[n[0]] = { roles: n[1..-1] }
|
56
|
+
end
|
57
|
+
|
58
|
+
cluster
|
59
|
+
end
|
60
|
+
|
61
|
+
# TODO: It never seems to hit "Marked #{patch} as patched for #{k}"
|
62
|
+
def parse_patch_status(ssh)
|
63
|
+
# Initialize 'patched' hash for each cluster node
|
64
|
+
@nodes.each_value do |node|
|
65
|
+
node[:patched] = Hash.new
|
66
|
+
manifest['patches'].keys.each { |patch| node[:patched][patch] = true }
|
67
|
+
end
|
68
|
+
|
69
|
+
status = ssh.exec_sc!('kato patch status')[:stdout]
|
70
|
+
log "kato patch status:\n#{status}"
|
71
|
+
|
72
|
+
patches = hashify_patch_status(status)
|
73
|
+
patches.each do |patch, info|
|
74
|
+
if info.empty?
|
75
|
+
@nodes.each do |k, v|
|
76
|
+
v[:patched][patch] = false
|
77
|
+
log "Marked #{patch} as patched for #{k}."
|
78
|
+
end
|
79
|
+
elsif info.has_key?(:unpatched)
|
80
|
+
@nodes.each do |k, v|
|
81
|
+
v[:patched][patch] = false if info[:unpatched].include?(k)
|
82
|
+
log "Marked #{patch} as unpatched for #{k}."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def hashify_patch_status(status)
|
89
|
+
patches = Hash.new
|
90
|
+
|
91
|
+
unless status =~ /^No Stackato updates are available for #{@version}\.$/
|
92
|
+
status = strip_footer(strip_header(status)).split("\n\n")
|
93
|
+
|
94
|
+
status.each do |patch|
|
95
|
+
patch = patch.split("\n").map { |p| p.lstrip.split(': ') }
|
96
|
+
name = patch[0][0]
|
97
|
+
patches[name] = Hash.new
|
98
|
+
|
99
|
+
patch.each do |line|
|
100
|
+
if line[0] == 'to be installed on'
|
101
|
+
list = line[1].lstrip
|
102
|
+
patches[name][:unpatched] = (list == 'none') ? [] : list.split(', ')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
patches
|
109
|
+
end
|
110
|
+
|
111
|
+
def strip_header(status)
|
112
|
+
log 'Stripping header'
|
113
|
+
|
114
|
+
s = status.split("\n")
|
115
|
+
major_ver = @version.to_i
|
116
|
+
header_end = case major_ver
|
117
|
+
when 2
|
118
|
+
/^Known updates for Stackato #{@version}:$/
|
119
|
+
when 3
|
120
|
+
/^\d+ updates are available for #{@version}.$/
|
121
|
+
end
|
122
|
+
|
123
|
+
break if s.shift.match header_end until s.empty?
|
124
|
+
raise InvalidPatchStatus, "kato patch status:\n\n#{status}" if s.empty?
|
125
|
+
|
126
|
+
s.shift if major_ver == 3 # Strip superfluous leading empty line
|
127
|
+
s.join("\n")
|
128
|
+
end
|
129
|
+
|
130
|
+
def strip_footer(status)
|
131
|
+
log 'Stripping footer'
|
132
|
+
|
133
|
+
s = status.split("\n")
|
134
|
+
until s.empty?
|
135
|
+
line = s.pop
|
136
|
+
if line =~ /^\t+.+: .+$/
|
137
|
+
s << line
|
138
|
+
break
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
raise InvalidPatchStatus, "kato patch status:\n\n#{status}" if s.empty?
|
143
|
+
s.join("\n")
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
class Stackadmin
|
7
|
+
attr_reader :id, :version, :license, :nodes, :fresh
|
8
|
+
|
9
|
+
def initialize(target, port = 22, debug = false, yml_file = nil)
|
10
|
+
@port = port
|
11
|
+
@debug = debug
|
12
|
+
yml_file ? parse_yaml(target, yml_file) : audit(target)
|
13
|
+
end
|
14
|
+
|
15
|
+
def refresh(target = nil)
|
16
|
+
@id ||= target
|
17
|
+
audit
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.find_instances(ssh_config = '~/.ssh/config', filter = [])
|
21
|
+
instances = IO.read(File.expand_path(ssh_config))
|
22
|
+
.scan(/^\s*Host (.*stackato.*)/)
|
23
|
+
.flatten
|
24
|
+
filter.each { |f| instances.delete_if { |host| host =~ /#{f}/ } }
|
25
|
+
instances
|
26
|
+
end
|
27
|
+
|
28
|
+
def manifest(uri = nil)
|
29
|
+
unless @manifest
|
30
|
+
uri ||= "https://get.stackato.com/kato-patch/#{@version}/manifest.json"
|
31
|
+
log "Retrieving latest manifest from: #{uri}"
|
32
|
+
end
|
33
|
+
|
34
|
+
@manifest ||= JSON.load(open(uri)) rescue nil
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def log(msg)
|
40
|
+
$stderr.puts ">> #{msg}" if @debug
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Stackadmin
|
2
|
+
class Exception < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class YAMLTargetNotFound < Stackadmin::Exception
|
6
|
+
end
|
7
|
+
|
8
|
+
class InvalidPatchStatus < Stackadmin::Exception
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidFlags < Stackadmin::Exception
|
12
|
+
end
|
13
|
+
|
14
|
+
class InvalidCommand < Stackadmin::Exception
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'net/ssh'
|
4
|
+
|
5
|
+
# Grab exit codes from remote commands
|
6
|
+
# Yanked this from http://stackoverflow.com/a/13436242
|
7
|
+
class Net::SSH::Connection::Session
|
8
|
+
class CommandFailed < Net::SSH::Exception
|
9
|
+
end
|
10
|
+
|
11
|
+
class CommandExecutionFailed < Net::SSH::Exception
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec_sc!(command)
|
15
|
+
stdout_data, stderr_data = '', ''
|
16
|
+
exit_code, exit_signal = nil, nil
|
17
|
+
self.open_channel do |channel|
|
18
|
+
channel.exec(command) do |_, success|
|
19
|
+
raise CommandExecutionFailed, "Command \"#{command}\" was unable to execute" unless success
|
20
|
+
|
21
|
+
channel.on_data do |_, data|
|
22
|
+
stdout_data += data
|
23
|
+
end
|
24
|
+
|
25
|
+
channel.on_extended_data do |_, _, data|
|
26
|
+
stderr_data += data
|
27
|
+
end
|
28
|
+
|
29
|
+
channel.on_request('exit-status') do |_, data|
|
30
|
+
exit_code = data.read_long
|
31
|
+
end
|
32
|
+
|
33
|
+
channel.on_request('exit-signal') do |_, data|
|
34
|
+
exit_signal = data.read_long
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self.loop
|
39
|
+
|
40
|
+
raise CommandFailed, "Command \"#{command}\" returned exit code #{exit_code}" unless exit_code == 0
|
41
|
+
|
42
|
+
{
|
43
|
+
stdout: stdout_data,
|
44
|
+
stderr: stderr_data,
|
45
|
+
exit_code: exit_code,
|
46
|
+
exit_signal: exit_signal
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'net-ssh'
|
5
|
+
require_relative 'exceptions'
|
6
|
+
require_relative 'audit'
|
7
|
+
|
8
|
+
class Stackadmin
|
9
|
+
# Returns hash of hashes: { node1: { installed: [patch1], not_installed: [patch2] }, etc }
|
10
|
+
# TODO: DRY this with ::mark! & ::reinstall! & ::revert!
|
11
|
+
def install!(patches = [], node = nil, local = false, restart = true, force_update = false, manifest_file = nil)
|
12
|
+
status = Hash.new
|
13
|
+
output = patch('install', patches, node: node, local: local, no_restart: !restart, force_update: force_update, manifest: manifest_file)
|
14
|
+
|
15
|
+
output.each do |p|
|
16
|
+
p = p[:stdout]
|
17
|
+
log "Job output:\n#{p}"
|
18
|
+
p.gsub!(/\e\[\d+m/, '') # Strip ASCII color
|
19
|
+
|
20
|
+
p.scan(/^Failed installing update (.+?) on (.+?)\.$/).each do |fp|
|
21
|
+
# fp = Array of failed patches
|
22
|
+
# e.g. [["patch1-name", "node1-ip"], ["patch2-name", "node2-ip"]]
|
23
|
+
status[fp[1]] = {
|
24
|
+
installed: [],
|
25
|
+
not_installed: []
|
26
|
+
} unless status.has_key?(fp[1])
|
27
|
+
|
28
|
+
status[fp[1]][:not_installed] << fp[0]
|
29
|
+
end
|
30
|
+
|
31
|
+
p.scan(/^Successfully installed update (.+?) on (.+?)\.$/).each do |sp|
|
32
|
+
# sp = Array of successful patches. See fp example above
|
33
|
+
status[sp[1]] = {
|
34
|
+
installed: [],
|
35
|
+
not_installed: []
|
36
|
+
} unless status.has_key?(sp[1])
|
37
|
+
|
38
|
+
status[sp[1]][:installed] << sp[0]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
status
|
43
|
+
end
|
44
|
+
|
45
|
+
def reset!(node = nil, local = false)
|
46
|
+
output = patch('reset', [], node: node, local: local)
|
47
|
+
output[0][:stdout] =~ /^Updates state reset.$/ ? true : false
|
48
|
+
end
|
49
|
+
|
50
|
+
def update!(node = nil, local = false, manifest_file = nil)
|
51
|
+
output = v3_action_check('update') do
|
52
|
+
patch('update', [], node: node, local: local, manifest: manifest_file)
|
53
|
+
end
|
54
|
+
|
55
|
+
output[0][:stdout] =~ /^done!$/ ? true : false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns hash: { node1: [patch1, patch2], etc }
|
59
|
+
# TODO: DRY this with ::install! & ::reinstall! & ::revert!
|
60
|
+
def mark!(patches = [], mark_installed = false, node = nil, local = false)
|
61
|
+
status = Hash.new
|
62
|
+
output = v3_action_check('mark') do
|
63
|
+
patch('mark', patches, node: node, local: local, mark_installed: mark_installed)
|
64
|
+
end
|
65
|
+
|
66
|
+
output.each do |p|
|
67
|
+
p = p[:stdout]
|
68
|
+
log "Job output:\n#{p}"
|
69
|
+
p.gsub!(/\e\[\d+m/, '') # Strip ASCII color
|
70
|
+
|
71
|
+
p.scan(/^Successfully marked patch (.+?) as #{'not ' unless mark_installed}installed on (.+?)\.$/).each do |sp|
|
72
|
+
# sp = Array of successfully marked patches
|
73
|
+
# e.g. [["patch1-name", "node1-ip"], ["patch2-name", "node2-ip"]]
|
74
|
+
status[sp[1]] = Array.new unless status.has_key?(sp[1])
|
75
|
+
status[sp[1]] << sp[0]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
status
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns hash of hashes: { node1: { installed: [patch1], not_installed: [patch2] }, etc }
|
83
|
+
# TODO: DRY this with ::install! & ::mark! & ::revert!
|
84
|
+
def reinstall!(patches = [], node = nil, local = false, restart = true, force_update = false, manifest_file = nil)
|
85
|
+
if patches.empty?
|
86
|
+
raise InvalidCommand, "kato patch reinstall requires a specific patchname!"
|
87
|
+
else
|
88
|
+
status = Hash.new
|
89
|
+
output = patch('reinstall', patches, node: node, local: local, no_restart: !restart, force_update: force_update, manifest: manifest_file)
|
90
|
+
|
91
|
+
output.each do |p|
|
92
|
+
p = p[:stdout]
|
93
|
+
log "Job output:\n#{p}"
|
94
|
+
p.gsub!(/\e\[\d+m/, '') # Strip ASCII color
|
95
|
+
|
96
|
+
p.scan(/^Failed installing update (.+?) on (.+?)\.$/).each do |fp|
|
97
|
+
# fp = Array of failed patches
|
98
|
+
# e.g. [["patch1-name", "node1-ip"], ["patch2-name", "node2-ip"]]
|
99
|
+
status[fp[1]] = {
|
100
|
+
installed: [],
|
101
|
+
not_installed: []
|
102
|
+
} unless status.has_key?(fp[1])
|
103
|
+
|
104
|
+
status[fp[1]][:not_installed] << fp[0]
|
105
|
+
end
|
106
|
+
|
107
|
+
p.scan(/^Successfully installed update (.+?) on (.+?)\.$/).each do |sp|
|
108
|
+
# sp = Array of successful patches. See fp example above
|
109
|
+
status[sp[1]] = {
|
110
|
+
installed: [],
|
111
|
+
not_installed: []
|
112
|
+
} unless status.has_key?(sp[1])
|
113
|
+
|
114
|
+
status[sp[1]][:installed] << sp[0]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
status
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns hash of hashes: { node1: { reverted: [patch1], not_reverted: [patch2], unable_to_revert: [patch3] }, etc }
|
123
|
+
# TODO: DRY this with ::install! & ::mark!
|
124
|
+
def revert!(patches = [], node = nil, local = false, restart = true, force_update = false, manifest_file = nil)
|
125
|
+
status = Hash.new
|
126
|
+
output = v3_action_check('revert') do
|
127
|
+
patch('revert', patches, node: node, local: local, no_restart: !restart, force_update: force_update, manifest: manifest_file)
|
128
|
+
end
|
129
|
+
|
130
|
+
output.each do |p|
|
131
|
+
p = p[:stdout]
|
132
|
+
log "Job output:\n#{p}"
|
133
|
+
p.gsub!(/\e\[\d+m/, '') # Strip ASCII color
|
134
|
+
|
135
|
+
p.scan(/^Failed reverting update (.+?) on (.+?)\.$/).each do |fp|
|
136
|
+
# fp = Array of failed patches
|
137
|
+
# e.g. [["patch1-name", "node1-ip"], ["patch2-name", "node2-ip"]]
|
138
|
+
status[fp[1]] = {
|
139
|
+
reverted: [],
|
140
|
+
not_reverted: [],
|
141
|
+
unable_to_revert: []
|
142
|
+
} unless status.has_key?(fp[1])
|
143
|
+
|
144
|
+
status[fp[1]][:not_installed] << fp[0]
|
145
|
+
end
|
146
|
+
|
147
|
+
p.scan(/^Successfully reverted update (.+?) on (.+?)\.$/).each do |sp|
|
148
|
+
# sp = Array of successful patches. See fp example above
|
149
|
+
status[sp[1]] = {
|
150
|
+
reverted: [],
|
151
|
+
not_reverted: [],
|
152
|
+
unable_to_revert: []
|
153
|
+
} unless status.has_key?(sp[1])
|
154
|
+
|
155
|
+
status[sp[1]][:installed] << sp[0]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
status
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def v3_action_check(action)
|
165
|
+
if @version.to_i == 3
|
166
|
+
yield
|
167
|
+
else
|
168
|
+
raise InvalidCommand, "kato patch #{action} unavailable on Stackato v#{@version}!"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Args:
|
173
|
+
# - action: string, required
|
174
|
+
# -- subcommand to use for 'kato patch'
|
175
|
+
# - patches: array
|
176
|
+
# -- list of patches to apply with action
|
177
|
+
# -- string accepted, but assumed to denote a single patch
|
178
|
+
# - opts: hash, optional
|
179
|
+
# -- flags/arguments to apply to the kato patch action
|
180
|
+
def patch(action, patches = [], opts = {})
|
181
|
+
raise InvalidFlags, "Can't target both #{opts[:node]} & \"local\"!" if (opts[:node] && opts[:local])
|
182
|
+
|
183
|
+
cmd = "kato patch #{action}"
|
184
|
+
|
185
|
+
if @version.to_i == 3
|
186
|
+
cmd += " --node #{opts[:node]}" if opts[:node]
|
187
|
+
cmd += " --manifest #{opts[:manifest]}" if opts[:manifest]
|
188
|
+
cmd += ' --force-update' if opts[:force_update]
|
189
|
+
|
190
|
+
if action == 'mark'
|
191
|
+
cmd += opts[:mark_installed] ? ' --installed' : ' --notinstalled'
|
192
|
+
end
|
193
|
+
|
194
|
+
if opts[:manifest] || opts[:force_update]
|
195
|
+
@manifest = nil
|
196
|
+
manifest(opts[:manifest])
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
cmd += ' --no-restart' if opts[:no_restart]
|
201
|
+
cmd += case @version.to_i
|
202
|
+
when 2
|
203
|
+
' --only-this-node'
|
204
|
+
when 3
|
205
|
+
' --local'
|
206
|
+
end if opts[:local]
|
207
|
+
|
208
|
+
patches = [patches] if patches.class == String
|
209
|
+
output = Array.new
|
210
|
+
|
211
|
+
begin
|
212
|
+
Net::SSH.start(@id, 'stackato', port: @port) do |ssh|
|
213
|
+
if patches.empty?
|
214
|
+
log "Starting job: #{cmd}"
|
215
|
+
output << ssh.exec_sc!(cmd)
|
216
|
+
else
|
217
|
+
patches.each do |patch|
|
218
|
+
log "Starting job: #{cmd} #{patch}"
|
219
|
+
output << ssh.exec_sc!("#{cmd} #{patch}")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
parse_patch_status(ssh)
|
224
|
+
end
|
225
|
+
rescue Net::SSH::Exception => e
|
226
|
+
patches = patches.empty? ? '[all]' : patches.to_s
|
227
|
+
log "kato patch failed! \"#{cmd} #{patches}\" -- #{e}"
|
228
|
+
end
|
229
|
+
|
230
|
+
output
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require_relative 'exceptions'
|
5
|
+
|
6
|
+
class Stackadmin
|
7
|
+
def to_yaml
|
8
|
+
{ 'id' => @id,
|
9
|
+
'version' => @version,
|
10
|
+
'license' => @license,
|
11
|
+
'nodes' => @nodes }.to_yaml
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parse_yaml(target, yml_file)
|
17
|
+
instances = YAML.load(File.read(yml_file))
|
18
|
+
|
19
|
+
instances.each do |stk|
|
20
|
+
if stk['id'] == target
|
21
|
+
@id = stk['id']
|
22
|
+
@version = stk['version']
|
23
|
+
@license = stk['license']
|
24
|
+
@nodes = stk['nodes']
|
25
|
+
@fresh = false
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
raise YAMLTargetNotFound, "Could not find '#{target}' in #{yml_file}!"
|
31
|
+
end
|
32
|
+
end
|
data/lib/stackadmin.rb
ADDED
data/test/Vagrantfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
# vi: set ft=ruby :
|
3
|
+
|
4
|
+
SSH_CONFIG = <<-EOS
|
5
|
+
Host *
|
6
|
+
StrictHostKeyChecking no
|
7
|
+
UserKnownHostsFile=/dev/null
|
8
|
+
EOS
|
9
|
+
|
10
|
+
Vagrant.configure(2) do |config|
|
11
|
+
config.vm.box = 'stackato-v3.4.2'
|
12
|
+
config.vm.box_url = 's3://deployination/stackato-v3.4.2.box'
|
13
|
+
config.vm.provision 'shell', inline: "echo '#{SSH_CONFIG}' > ~stackato/.ssh/config"
|
14
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'minitest/reporters'
|
3
|
+
require_relative '../lib/stackadmin'
|
4
|
+
|
5
|
+
MiniTest::Reporters.use! [MiniTest::Reporters::SpecReporter.new]
|
6
|
+
|
7
|
+
TEST_ENV = '127.0.0.1'
|
8
|
+
|
9
|
+
describe Stackadmin do
|
10
|
+
subject { Stackadmin.new(TEST_ENV, 2222) }
|
11
|
+
|
12
|
+
describe 'instantiation' do
|
13
|
+
it 'requires a target argument' do
|
14
|
+
lambda { Stackadmin.new }.must_raise ArgumentError
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'requires a valid target' do
|
18
|
+
lambda { Stackadmin.new('foobaz') }.must_raise SocketError
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'can connect to a valid Stackato instance' do
|
22
|
+
subject.must_be_instance_of Stackadmin
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'defaults to auditing a live deployment' do
|
26
|
+
assert subject.fresh
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'auditing' do
|
31
|
+
it 'retrieves the Stackato version' do
|
32
|
+
subject.version.must_equal '3.4.2'
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'retrieves the active license info' do
|
36
|
+
subject.license.must_be_instance_of Hash
|
37
|
+
subject.license.must_be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'retrieves the latest manifest from ActiveState' do
|
41
|
+
subject.manifest.must_be_instance_of Hash
|
42
|
+
subject.manifest['stackato_version'].must_equal subject.version
|
43
|
+
%w( kato_patch patches ).each do |k|
|
44
|
+
subject.manifest(k).must_be_instance_of Hash
|
45
|
+
subject.manifest(k).wont_be_empty
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# TODO: We should be testing more than a single-node microcloud
|
50
|
+
describe 'nodes' do
|
51
|
+
it 'maps cluster nodes to a hash' do
|
52
|
+
subject.nodes.must_be_instance_of Hash
|
53
|
+
subject.nodes.size.must_equal 1
|
54
|
+
end
|
55
|
+
|
56
|
+
let(:local_node) { subject.nodes[TEST_ENV] }
|
57
|
+
|
58
|
+
it 'retrieves role info for each node' do
|
59
|
+
subject.nodes.each_value do |node|
|
60
|
+
node[:roles].must_be_instance_of Array
|
61
|
+
node[:roles].wont_be_empty
|
62
|
+
end
|
63
|
+
|
64
|
+
local_node[:roles].must_include 'base'
|
65
|
+
local_node[:roles].must_include 'primary'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'retrieves patch info for each node' do
|
69
|
+
subject.nodes.each_value do |node|
|
70
|
+
node[:patched].must_be_instance_of Hash
|
71
|
+
node[:patched].wont_be_empty
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'patching' do
|
78
|
+
describe 'install!' do
|
79
|
+
it 'all patches to all nodes by default' do
|
80
|
+
skip 'need to isolate environments for testing'
|
81
|
+
|
82
|
+
subject.nodes.each_value do |node|
|
83
|
+
node[:patched].each_value { |patch| refute patch }
|
84
|
+
end
|
85
|
+
|
86
|
+
subject.install!
|
87
|
+
|
88
|
+
subject.nodes.each_value do |node|
|
89
|
+
node[:patched].each_value { |patch| assert patch }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
let(:local_node) { subject.nodes[TEST_ENV] }
|
94
|
+
|
95
|
+
it 'install one patch' do
|
96
|
+
skip 'need to isolate environments for testing'
|
97
|
+
|
98
|
+
patch = local_node[:patched].keys[0]
|
99
|
+
subject.nodes.each_value { |node| refute node[:patched][patch] }
|
100
|
+
subject.install! patch
|
101
|
+
subject.nodes.each_value { |node| assert node[:patched][patch] }
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'install multiple patches' do
|
105
|
+
skip 'need to isolate environments for testing'
|
106
|
+
|
107
|
+
patches = local_node[:patched].keys[0..1]
|
108
|
+
patches.each do |patch|
|
109
|
+
subject.nodes.each_value { |node| refute node[:patched][patch] }
|
110
|
+
end
|
111
|
+
|
112
|
+
subject.install! patches
|
113
|
+
|
114
|
+
patches.each do |patch|
|
115
|
+
subject.nodes.each_value { |node| assert node[:patched][patch] }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'only to the local node' do
|
120
|
+
skip 'need to test multi-node clusters'
|
121
|
+
|
122
|
+
local_node[:patched].each_value { |patch| refute patch }
|
123
|
+
subject.install!([], nil, true)
|
124
|
+
local_node[:patched].each_value { |patch| assert patch }
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'only to a specified node' do
|
128
|
+
skip 'need to test multi-node clusters'
|
129
|
+
|
130
|
+
local_node[:patched].each_value { |patch| refute patch }
|
131
|
+
subject.install!([], TEST_ENV)
|
132
|
+
local_node[:patched].each_value { |patch| assert patch }
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'only to multiple specified nodes' do
|
136
|
+
skip 'Not implemented yet'
|
137
|
+
|
138
|
+
nodes = subject.nodes.keys[0..1]
|
139
|
+
nodes.each do |node|
|
140
|
+
node[:patched].each_value { |patch| refute patch }
|
141
|
+
end
|
142
|
+
|
143
|
+
subject.install!([], nodes)
|
144
|
+
|
145
|
+
nodes.each do |node|
|
146
|
+
node[:patched].each_value { |patch| assert patch }
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'without restarting the node' do
|
151
|
+
skip 'need to isolate environments for testing'
|
152
|
+
|
153
|
+
subject.nodes.each_value do |node|
|
154
|
+
node[:patched].each_value { |patch| refute patch }
|
155
|
+
end
|
156
|
+
|
157
|
+
subject.install!([], nil, false, false)
|
158
|
+
|
159
|
+
subject.nodes.each_value do |node|
|
160
|
+
node[:patched].each_value { |patch| assert patch }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'detects a successful patch installation' do
|
165
|
+
skip
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'detects a failed patch installation' do
|
169
|
+
skip
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe 'reset! marks all patches as uninstalled' do
|
174
|
+
it 'on all nodes by default' do
|
175
|
+
skip
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'only on the local node' do
|
179
|
+
skip
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'only on a specified node' do
|
183
|
+
skip
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'only on multiple specified nodes' do
|
187
|
+
skip 'Not implemented yet'
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'detects a successful reset' do
|
191
|
+
skip
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe 'update! refreshes the manifest' do
|
196
|
+
it 'on all nodes by default' do
|
197
|
+
skip
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'only on the local node' do
|
201
|
+
skip
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'only on a specified node' do
|
205
|
+
skip
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'only on multiple specified nodes' do
|
209
|
+
skip 'Not implemented yet'
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'detects a successful update' do
|
213
|
+
skip
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe 'mark!' do
|
218
|
+
describe 'uninstalled' do
|
219
|
+
it 'single patch on all nodes by default' do
|
220
|
+
skip
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'multiple patches' do
|
224
|
+
skip
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'only on the local node' do
|
228
|
+
skip
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'only on a specified node' do
|
232
|
+
skip
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'only on multiple specified nodes' do
|
236
|
+
skip 'Not implemented yet'
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'installed on all nodes' do
|
241
|
+
skip
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'detects a successful mark' do
|
245
|
+
skip
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe 'reinstall!' do
|
250
|
+
it 'single patch to all nodes by default' do
|
251
|
+
skip
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'only multiple specified patches' do
|
255
|
+
skip
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'only to the local node' do
|
259
|
+
skip
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'only to a specified node' do
|
263
|
+
skip
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'only to multiple specified nodes' do
|
267
|
+
skip 'Not implemented yet'
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'without restarting the node' do
|
271
|
+
skip
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'detects a successful reinstallation' do
|
275
|
+
skip
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'detects a failed reinstallation' do
|
279
|
+
skip
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe 'revert!' do
|
284
|
+
it 'all patches on all nodes by default' do
|
285
|
+
skip
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'only one patch' do
|
289
|
+
skip
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'only multiple specified patches' do
|
293
|
+
skip
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'only to the local node' do
|
297
|
+
skip
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'only to a specified node' do
|
301
|
+
skip
|
302
|
+
end
|
303
|
+
|
304
|
+
it 'only to multiple specified nodes' do
|
305
|
+
skip 'Not implemented yet'
|
306
|
+
end
|
307
|
+
|
308
|
+
it 'without restarting the node' do
|
309
|
+
skip
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'detects a successful revert' do
|
313
|
+
skip
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'detects a failed revert' do
|
317
|
+
skip
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'detects a patch this is unable to be reverted' do
|
321
|
+
skip 'Not implemented yet'
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
metadata
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stackadmin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andres Rojas
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-ssh
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-reporters
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: vagrant
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: vagrant-s3auth
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- andres.rojas@mtnsat.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- Gemfile
|
120
|
+
- Gemfile.lock
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- lib/stackadmin.rb
|
124
|
+
- lib/stackadmin/audit.rb
|
125
|
+
- lib/stackadmin/base.rb
|
126
|
+
- lib/stackadmin/exceptions.rb
|
127
|
+
- lib/stackadmin/net-ssh.rb
|
128
|
+
- lib/stackadmin/patch.rb
|
129
|
+
- lib/stackadmin/version.rb
|
130
|
+
- lib/stackadmin/yaml.rb
|
131
|
+
- test/Vagrantfile
|
132
|
+
- test/stackadmin_test.rb
|
133
|
+
homepage: https://github.com/mtnsat/stackadmin
|
134
|
+
licenses: []
|
135
|
+
metadata: {}
|
136
|
+
post_install_message:
|
137
|
+
rdoc_options: []
|
138
|
+
require_paths:
|
139
|
+
- lib
|
140
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - '>='
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
requirements: []
|
151
|
+
rubyforge_project:
|
152
|
+
rubygems_version: 2.0.14
|
153
|
+
signing_key:
|
154
|
+
specification_version: 4
|
155
|
+
summary: A helper class for administering Stackato deployments
|
156
|
+
test_files:
|
157
|
+
- test/Vagrantfile
|
158
|
+
- test/stackadmin_test.rb
|