stackadmin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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