stackadmin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ # stackadmin
2
+ Gem to facilitate administration of Stackato deployments
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,5 @@
1
+ # encoding: utf-8
2
+
3
+ class Stackadmin
4
+ VERSION = '0.1.0'
5
+ 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
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Utility class for administering Stackato nodes & clusters.
4
+
5
+ %w( version net-ssh base exceptions audit yaml patch ).each do |rb|
6
+ require_relative "stackadmin/#{rb}"
7
+ end
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