knife-pkg 0.0.1

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.
@@ -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