knife-pkg 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,100 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ class Chef
18
+ class Knife
19
+ class PkgShowUpdates < PkgBase
20
+
21
+ banner 'knife pkg show updates QUERY (options)'
22
+
23
+ deps do
24
+ require 'net/ssh'
25
+ require 'net/ssh/multi'
26
+ require 'chef/knife/ssh'
27
+ require 'knife-pkg'
28
+ end
29
+
30
+ option :attribute,
31
+ :short => "-a ATTR",
32
+ :long => "--attribute ATTR",
33
+ :description => "The attribute to use for opening the connection - default depends on the context",
34
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_attribute] = key.strip }
35
+
36
+ option :ssh_user,
37
+ :short => "-x USERNAME",
38
+ :long => "--ssh-user USERNAME",
39
+ :description => "The ssh username"
40
+
41
+ option :ssh_password,
42
+ :short => "-P PASSWORD",
43
+ :long => "--ssh-password PASSWORD",
44
+ :description => "The ssh password"
45
+
46
+ option :ssh_port,
47
+ :short => "-p PORT",
48
+ :long => "--ssh-port PORT",
49
+ :description => "The ssh port",
50
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key.strip }
51
+
52
+ option :ssh_gateway,
53
+ :short => "-G GATEWAY",
54
+ :long => "--ssh-gateway GATEWAY",
55
+ :description => "The ssh gateway",
56
+ :proc => Proc.new { |key| Chef::Config[:knife][:ssh_gateway] = key.strip }
57
+
58
+ option :forward_agent,
59
+ :short => "-A",
60
+ :long => "--forward-agent",
61
+ :description => "Enable SSH agent forwarding",
62
+ :boolean => true
63
+
64
+ option :identity_file,
65
+ :short => "-i IDENTITY_FILE",
66
+ :long => "--identity-file IDENTITY_FILE",
67
+ :description => "The SSH identity file used for authentication"
68
+
69
+ option :host_key_verify,
70
+ :long => "--[no-]host-key-verify",
71
+ :description => "Verify host key, enabled by default.",
72
+ :boolean => true,
73
+ :default => true
74
+
75
+ option :sudo_required,
76
+ :short => "-z",
77
+ :long => "--sudo-required",
78
+ :description => "Use sudo",
79
+ :boolean => true,
80
+ :default => false
81
+
82
+ option :pkg_verbose,
83
+ :short => "-l",
84
+ :long => "--pkg-verbose",
85
+ :description => "More verbose output for package related things",
86
+ :boolean => true,
87
+ :default => false
88
+
89
+
90
+ def run
91
+ super
92
+ end
93
+
94
+ def process(node, session)
95
+ ui.info("===> " + extract_nested_value(node, config[:attribute]))
96
+ ::Knife::Pkg::PackageController.available_updates(node, session, pkg_options)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,66 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'knife-pkg'
18
+
19
+ module Knife
20
+ module Pkg
21
+ class DebianPackageController < PackageController
22
+
23
+ def initialize(node, session, opts = {})
24
+ super(node, session, opts)
25
+ end
26
+
27
+ def update_pkg_cache
28
+ ShellCommand.exec("#{sudo}apt-get update", @session)
29
+ end
30
+
31
+ def last_pkg_cache_update
32
+ result = ShellCommand.exec("stat -c %y /var/lib/apt/periodic/update-success-stamp", @session)
33
+ Time.parse(result.stdout.chomp)
34
+ end
35
+
36
+ def installed_version(package)
37
+ ShellCommand.exec("dpkg -p #{package.name} | grep -i Version: | awk '{print $2}'", @session).stdout.chomp
38
+ end
39
+
40
+ def available_updates
41
+ packages = Array.new
42
+ if !update_notifier_installed?
43
+ raise RuntimeError, "Gna!! No update-notifier(-common) installed!? Go ahead, install it and come back!"
44
+ else
45
+ result = ShellCommand.exec("#{sudo}/usr/lib/update-notifier/apt_check.py -p", @session)
46
+ result.stderr.split("\n").each do |item|
47
+ package = Package.new(item)
48
+ package.version = installed_version(package)
49
+ packages << package
50
+ end
51
+ end
52
+ packages
53
+ end
54
+
55
+ def update_package!(package)
56
+ cmd_string = "#{sudo} DEBIAN_FRONTEND=noninteractive apt-get install #{package.name} -y -o Dpkg::Options::='--force-confold'"
57
+ cmd_string += " -s" if @options[:dry_run]
58
+ ShellCommand.exec(cmd_string, @session)
59
+ end
60
+
61
+ def update_notifier_installed?
62
+ ShellCommand.exec("dpkg-query -W update-notifier-common 2>/dev/null || echo 'false'", @session).stdout.chomp != 'false'
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,171 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'knife-pkg'
18
+ require 'chef/knife'
19
+
20
+ module Knife
21
+ module Pkg
22
+ class PackageController
23
+
24
+ attr_accessor :node
25
+ attr_accessor :session
26
+ attr_accessor :options
27
+ attr_accessor :ui
28
+
29
+ def initialize(node, session, opts = {})
30
+ @node = node
31
+ @session = session
32
+ @options = opts
33
+ end
34
+
35
+ def self.ui
36
+ @ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
37
+ end
38
+
39
+ def sudo
40
+ @options[:sudo] ? 'sudo ' : ''
41
+ end
42
+
43
+ ## ++ methods to implement
44
+
45
+ # update the package cache
46
+ # e.g apt-get update
47
+ def update_pkg_cache
48
+ raise NotImplementedError
49
+ end
50
+
51
+ # returns the `Time` of the last package cache update
52
+ def last_pkg_cache_update
53
+ raise NotImplementedError
54
+ end
55
+
56
+ # returns the version string of the installed package
57
+ def installed_version(package)
58
+ raise NotImplementedError
59
+ end
60
+
61
+ # returns an `Array` of all available updates
62
+ def available_updates
63
+ raise NotImplementedError
64
+ end
65
+
66
+ # updates a package
67
+ # should only execute a 'dry-run' if @options[:dry_run] is set
68
+ # returns a ShellCommandResult
69
+ def update_package!(package)
70
+ raise NotImplementedError
71
+ end
72
+
73
+ ## ++ methods to implement
74
+
75
+
76
+ def update_package_verbose!(package)
77
+ result = update_package!(package)
78
+ if @options[:dry_run] || @options[:verbose]
79
+ ui.info(result.stdout)
80
+ ui.error(result.stderr)
81
+ end
82
+ end
83
+
84
+ def try_update_pkg_cache
85
+ if Time.now - last_pkg_cache_update > 86400 # 24 hours
86
+ @ui.info("Updating package cache...")
87
+ update_pkg_cache
88
+ end
89
+ end
90
+
91
+ def update_dialog(packages)
92
+ return if packages.count == 0
93
+
94
+ ui.info("\tThe following updates are available:") if packages.count > 0
95
+ packages.each do |package|
96
+ ui.info(ui.color("\t" + package.to_s, :yellow))
97
+ end
98
+
99
+ if UserDecision.yes?("\tDo you want to update all packages? [y|n]: ")
100
+ ui.info("\tupdating...")
101
+ packages.each do |p|
102
+ update_package_verbose!(p)
103
+ end
104
+ ui.info("\tall packages updated!")
105
+ else
106
+ packages.each do |package|
107
+ if UserDecision.yes?("\tDo you want to update #{package}? [y|n]: ")
108
+ result = update_package_verbose!(package)
109
+ ui.info("\t#{package} updated!")
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def self.list_available_updates(updates)
116
+ updates.each do |update|
117
+ ui.info(ui.color("\t" + update.to_s, :yellow))
118
+ end
119
+ end
120
+
121
+ def self.update!(node, session, packages, opts)
122
+ ctrl = self.init_controller(node, session, opts)
123
+
124
+ auto_updates = packages.map { |u| Package.new(u) }
125
+ updates_for_dialog = Array.new
126
+
127
+ ctrl.try_update_pkg_cache
128
+ available_updates = ctrl.available_updates
129
+
130
+ available_updates.each do |avail|
131
+ if auto_updates.select { |p| p.name == avail.name }.count == 0
132
+ updates_for_dialog << avail
133
+ else
134
+ ui.info("\tUpdating #{avail.to_s}")
135
+ ctrl.update_package_verbose!(avail)
136
+ end
137
+ end
138
+
139
+ ctrl.update_dialog(updates_for_dialog)
140
+ end
141
+
142
+ def self.available_updates(node, session, opts = {})
143
+ ctrl = self.init_controller(node, session, opts)
144
+ ctrl.try_update_pkg_cache
145
+ updates = ctrl.available_updates
146
+ list_available_updates(updates)
147
+ end
148
+
149
+ def self.init_controller(node, session, opts)
150
+ begin
151
+ ctrl_name = self.controller_name(node.platform)
152
+ require File.join(File.dirname(__FILE__), ctrl_name)
153
+ rescue LoadError
154
+ raise NotImplementedError, "I'm sorry, but #{node.platform} is not supported!"
155
+ end
156
+ ctrl = Object.const_get('Knife').const_get('Pkg').const_get("#{ctrl_name.capitalize}PackageController").new(node, session, opts)
157
+ ctrl.ui = self.ui
158
+ ctrl
159
+ end
160
+
161
+ def self.controller_name(platform)
162
+ case platform
163
+ when 'debian', 'ubuntu'
164
+ 'debian'
165
+ else
166
+ platform
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,42 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'knife-pkg'
18
+
19
+ module Knife
20
+ module Pkg
21
+ class Package
22
+ attr_accessor :name, :version
23
+
24
+ def initialize(name, version = '0.0')
25
+ @name = name.strip
26
+ @version = version
27
+ end
28
+
29
+ def to_s
30
+ @name + (version_to_s == '' ? '' : " #{version_to_s}")
31
+ end
32
+
33
+ def version_to_s
34
+ if @version.to_s != '0.0'
35
+ "(#{@version})"
36
+ else
37
+ ''
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,61 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ module Knife
18
+ module Pkg
19
+ class ShellCommand
20
+
21
+ def self.exec(cmd, session)
22
+
23
+ stdout_data, stderr_data = "", ""
24
+ exit_code, exit_signal = nil, nil
25
+ session.open_channel do |channel|
26
+ channel.exec(cmd) do |_, success|
27
+ raise RuntimeError, "Command \"#{@cmd}\" could not be executed!" if !success
28
+
29
+ channel.on_data do |_, data|
30
+ stdout_data += data
31
+ end
32
+
33
+ channel.on_extended_data do |_,_,data|
34
+ stderr_data += data
35
+ end
36
+
37
+ channel.on_request("exit-status") do |_,data|
38
+ exit_code = data.read_long
39
+ end
40
+
41
+ channel.on_request("exit-signal") do |_, data|
42
+ exit_signal = data.read_long
43
+ end
44
+ end
45
+ end
46
+ session.loop
47
+
48
+ result = ShellCommandResult.new(cmd, stdout_data, stderr_data, exit_code.to_i)
49
+
50
+ raise_error!(result) unless result.succeeded?
51
+
52
+ return result
53
+ end
54
+
55
+ def self.raise_error!(result)
56
+ raise RuntimeError, "Command failed! #{result.to_s}"
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,43 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ module Knife
18
+ module Pkg
19
+ class ShellCommandResult
20
+
21
+ attr_accessor :cmd
22
+ attr_accessor :stdout
23
+ attr_accessor :stderr
24
+ attr_accessor :exit_code
25
+
26
+ def initialize(cmd, stdout, stderr, exit_code)
27
+ @cmd = cmd
28
+ @stdout = stdout
29
+ @stderr = stderr
30
+ @exit_code = exit_code
31
+ end
32
+
33
+ def to_s
34
+ return "Command: \"#{@cmd}\", stdout: \"#{@stdout}\", stderr: \"#{@stderr}\", exit_code: \"#{@exit_code}\""
35
+ end
36
+
37
+ def succeeded?
38
+ return @exit_code.to_i == 0 ? true : false
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'chef/knife'
18
+
19
+ module Knife
20
+ module Pkg
21
+ class UserDecision
22
+
23
+ def self.ui
24
+ @ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
25
+ end
26
+
27
+ def self.yes?(text)
28
+ decision = false
29
+ while true
30
+ response = ui.ask_question("#{text}", :default => false)
31
+ case response
32
+ when 'y'
33
+ decision = true
34
+ break
35
+ when 'n'
36
+ decision = false
37
+ break
38
+ end
39
+ end
40
+ decision
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ module Knife
2
+ module Pkg
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
data/lib/knife-pkg.rb ADDED
@@ -0,0 +1,28 @@
1
+ #
2
+ # Copyright 2013, Holger Amann <holger@fehu.org>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ require 'knife-pkg/version'
18
+ require 'knife-pkg/package'
19
+ require 'knife-pkg/shell_command'
20
+ require 'knife-pkg/shell_command_result'
21
+ require 'knife-pkg/user_decision'
22
+ require 'knife-pkg/controllers/package_controller'
23
+
24
+ module Knife
25
+ module Pkg
26
+ # Your code goes here...
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ require 'knife-pkg'
2
+ require 'knife-pkg/controllers/debian'
3
+
4
+ include Knife::Pkg
5
+
6
+ describe 'DebianPackageController' do
7
+ describe '#new' do
8
+ it 'should create an instance of DebianPkgCtrl' do
9
+ p = DebianPackageController.new('a', 'b', :h => 1)
10
+ expect(p).to be_an_instance_of(DebianPackageController)
11
+ expect(p.node).to eq('a')
12
+ expect(p.session).to eq('b')
13
+ expect(p.options).to eq(:h => 1)
14
+ end
15
+ end
16
+
17
+ describe "#last_pkg_cache_update" do
18
+ it 'should return a time object' do
19
+ t = Time.now
20
+ result = ShellCommandResult.new(nil,"2013-10-07 09:58:34.000000000 +0200\n",nil,nil)
21
+ ShellCommand.stub(:exec).and_return(result)
22
+
23
+ p = DebianPackageController.new(nil, nil)
24
+ expect(p.last_pkg_cache_update).to be_an_instance_of Time
25
+ expect(p.last_pkg_cache_update).to eq(Time.parse("2013-10-07 09:58:34.000000000 +0200"))
26
+ end
27
+ end
28
+
29
+ describe "#available_updates" do
30
+ it 'should raise an error if update-notifier is not installed' do
31
+ p = DebianPackageController.new(nil, nil)
32
+ p.stub(:update_notifier_installed?).and_return(false)
33
+ expect{p.available_updates}.to raise_error(/update-notifier/)
34
+ end
35
+
36
+ it 'should return an array' do
37
+ result = ShellCommandResult.new(nil, nil, "1\n2\n3", nil)
38
+ ShellCommand.stub(:exec).and_return(result)
39
+ p = DebianPackageController.new(nil, nil)
40
+ p.stub(:update_notifier_installed?).and_return(true)
41
+ p.stub(:installed_version).and_return("1.0.0")
42
+ expect(p.available_updates).to be_an_instance_of Array
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ require 'knife-pkg'
2
+
3
+ include Knife::Pkg
4
+
5
+ describe 'PackageController' do
6
+ describe '#new' do
7
+ end
8
+
9
+ describe '#sudo' do
10
+ it 'should return sudo prefix' do
11
+ p = PackageController.new(nil, nil, :sudo => true)
12
+ expect(p.sudo).to eq("sudo ")
13
+ end
14
+
15
+ it 'should return no sudo prefix' do
16
+ p = PackageController.new(nil, nil)
17
+ expect(p.sudo).to eq("")
18
+ end
19
+ end
20
+
21
+ describe '.init_controller' do
22
+ it 'should initialize the right package controller' do
23
+ FakeNode = Struct.new(:platform)
24
+ node = FakeNode.new("debian")
25
+ PackageController.init_controller(node, nil, nil)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ require 'knife-pkg'
2
+
3
+ include Knife::Pkg
4
+
5
+ describe 'Package' do
6
+ describe '#new' do
7
+ it 'should create an instance of Package' do
8
+ p = Package.new('test')
9
+ expect(p).to be_an_instance_of Package
10
+ expect(p.version).to eq('0.0')
11
+ expect(p.name).to eq('test')
12
+ end
13
+ end
14
+
15
+ describe '#version_to_s' do
16
+ it 'should return the version' do
17
+ p = Package.new('', '0.0.1')
18
+ expect(p.version_to_s).to eq('(0.0.1)')
19
+ end
20
+
21
+ it 'should return an empty string if version is not defined' do
22
+ p = Package.new('')
23
+ expect(p.version_to_s).to eq('')
24
+ end
25
+ end
26
+
27
+ describe '#to_s' do
28
+ it 'should return package name with version' do
29
+ p = Package.new('test','0.0.1')
30
+ expect(p.to_s).to eq('test (0.0.1)')
31
+ end
32
+
33
+ it 'should return package without version' do
34
+ p = Package.new('test')
35
+ expect(p.to_s).to eq('test')
36
+ end
37
+ end
38
+
39
+
40
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+ require 'knife-pkg'
3
+
4
+ RSpec.configure do |config|
5
+ config.color_enabled = true
6
+ config.formatter = 'documentation'
7
+ end