poise-python 1.0.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 +7 -0
- data/.gitignore +11 -0
- data/.kitchen.travis.yml +9 -0
- data/.kitchen.yml +8 -0
- data/.travis.yml +21 -0
- data/.yardopts +7 -0
- data/Berksfile +28 -0
- data/Gemfile +33 -0
- data/LICENSE +201 -0
- data/README.md +399 -0
- data/Rakefile +17 -0
- data/chef/attributes/default.rb +24 -0
- data/chef/recipes/default.rb +20 -0
- data/lib/poise_python.rb +25 -0
- data/lib/poise_python/cheftie.rb +18 -0
- data/lib/poise_python/error.rb +23 -0
- data/lib/poise_python/python_command_mixin.rb +45 -0
- data/lib/poise_python/python_providers.rb +35 -0
- data/lib/poise_python/python_providers/base.rb +177 -0
- data/lib/poise_python/python_providers/portable_pypy.rb +96 -0
- data/lib/poise_python/python_providers/scl.rb +77 -0
- data/lib/poise_python/python_providers/system.rb +86 -0
- data/lib/poise_python/resources.rb +31 -0
- data/lib/poise_python/resources/pip_requirements.rb +102 -0
- data/lib/poise_python/resources/python_execute.rb +83 -0
- data/lib/poise_python/resources/python_package.rb +322 -0
- data/lib/poise_python/resources/python_runtime.rb +114 -0
- data/lib/poise_python/resources/python_runtime_pip.rb +167 -0
- data/lib/poise_python/resources/python_runtime_test.rb +185 -0
- data/lib/poise_python/resources/python_virtualenv.rb +164 -0
- data/lib/poise_python/utils.rb +63 -0
- data/lib/poise_python/utils/python_encoder.rb +73 -0
- data/lib/poise_python/version.rb +20 -0
- data/poise-python.gemspec +41 -0
- data/test/cookbooks/poise-python_test/metadata.rb +18 -0
- data/test/cookbooks/poise-python_test/recipes/default.rb +40 -0
- data/test/gemfiles/chef-12.gemfile +19 -0
- data/test/gemfiles/master.gemfile +23 -0
- data/test/integration/default/serverspec/default_spec.rb +102 -0
- data/test/spec/python_command_mixin_spec.rb +115 -0
- data/test/spec/python_providers/portable_pypy_spec.rb +68 -0
- data/test/spec/python_providers/scl_spec.rb +75 -0
- data/test/spec/python_providers/system_spec.rb +81 -0
- data/test/spec/resources/pip_requirements_spec.rb +69 -0
- data/test/spec/resources/python_package_spec.rb +65 -0
- data/test/spec/resources/python_runtime_pip_spec.rb +33 -0
- data/test/spec/resources/python_virtualenv_spec.rb +103 -0
- data/test/spec/spec_helper.rb +19 -0
- data/test/spec/utils/python_encoder_spec.rb +79 -0
- data/test/spec/utils_spec.rb +86 -0
- metadata +170 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
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/resource'
|
18
|
+
require 'poise_languages'
|
19
|
+
|
20
|
+
require 'poise_python/error'
|
21
|
+
require 'poise_python/python_providers/base'
|
22
|
+
|
23
|
+
|
24
|
+
module PoisePython
|
25
|
+
module PythonProviders
|
26
|
+
class Scl < Base
|
27
|
+
include PoiseLanguages::Scl::Mixin
|
28
|
+
provides(:scl)
|
29
|
+
scl_package('3.4.2', 'rh-python34', {
|
30
|
+
['redhat', 'centos'] => {
|
31
|
+
'~> 7.0' => 'https://www.softwarecollections.org/en/scls/rhscl/rh-python34/epel-7-x86_64/download/rhscl-rh-python34-epel-7-x86_64.noarch.rpm',
|
32
|
+
'~> 6.0' => 'https://www.softwarecollections.org/en/scls/rhscl/rh-python34/epel-6-x86_64/download/rhscl-rh-python34-epel-6-x86_64.noarch.rpm',
|
33
|
+
},
|
34
|
+
})
|
35
|
+
scl_package('3.3.2', 'python33', {
|
36
|
+
['redhat', 'centos'] => {
|
37
|
+
'~> 7.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/epel-7-x86_64/download/rhscl-python33-epel-7-x86_64.noarch.rpm',
|
38
|
+
'~> 6.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/epel-6-x86_64/download/rhscl-python33-epel-6-x86_64.noarch.rpm',
|
39
|
+
},
|
40
|
+
'fedora' => {
|
41
|
+
'~> 21.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/fedora-21-x86_64/download/rhscl-python33-fedora-21-x86_64.noarch.rpm',
|
42
|
+
'~> 20.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python33/fedora-20-x86_64/download/rhscl-python33-fedora-20-x86_64.noarch.rpm',
|
43
|
+
},
|
44
|
+
})
|
45
|
+
scl_package('2.7.8', 'python27', {
|
46
|
+
['redhat', 'centos'] => {
|
47
|
+
'~> 7.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/epel-7-x86_64/download/rhscl-python27-epel-7-x86_64.noarch.rpm',
|
48
|
+
'~> 6.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/epel-6-x86_64/download/rhscl-python27-epel-6-x86_64.noarch.rpm',
|
49
|
+
},
|
50
|
+
'fedora' => {
|
51
|
+
'~> 21.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/fedora-21-x86_64/download/rhscl-python27-fedora-21-x86_64.noarch.rpm',
|
52
|
+
'~> 20.0' => 'https://www.softwarecollections.org/en/scls/rhscl/python27/fedora-20-x86_64/download/rhscl-python27-fedora-20-x86_64.noarch.rpm',
|
53
|
+
},
|
54
|
+
})
|
55
|
+
|
56
|
+
def python_binary
|
57
|
+
::File.join(scl_folder, 'root', 'usr', 'bin', 'python')
|
58
|
+
end
|
59
|
+
|
60
|
+
def python_environment
|
61
|
+
scl_environment
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def install_python
|
67
|
+
install_scl_package
|
68
|
+
end
|
69
|
+
|
70
|
+
def uninstall_python
|
71
|
+
uninstall_scl_package
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
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/resource'
|
18
|
+
require 'poise_languages'
|
19
|
+
|
20
|
+
require 'poise_python/error'
|
21
|
+
require 'poise_python/python_providers/base'
|
22
|
+
|
23
|
+
|
24
|
+
module PoisePython
|
25
|
+
module PythonProviders
|
26
|
+
class System < Base
|
27
|
+
include PoiseLanguages::System::Mixin
|
28
|
+
provides(:system)
|
29
|
+
packages('python', {
|
30
|
+
debian: {
|
31
|
+
'8' => %w{python3.4 python2.7},
|
32
|
+
'7' => %w{python3.2 python2.7 python2.6},
|
33
|
+
'6' => %w{python3.1 python2.6 python2.5},
|
34
|
+
},
|
35
|
+
ubuntu: {
|
36
|
+
'14.04' => %w{python3.4 python2.7},
|
37
|
+
'12.04' => %w{python3.2 python2.7},
|
38
|
+
'10.04' => %w{python3.1 python2.6},
|
39
|
+
},
|
40
|
+
rhel: {default: %w{python}},
|
41
|
+
centos: {default: %w{python}},
|
42
|
+
fedora: {default: %w{python3 python}},
|
43
|
+
amazon: {default: %w{python27 python26 python}},
|
44
|
+
})
|
45
|
+
|
46
|
+
# Output value for the Python binary we are installing. Seems to match
|
47
|
+
# package name on all platforms I've checked.
|
48
|
+
def python_binary
|
49
|
+
::File.join('', 'usr', 'bin', system_package_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def install_python
|
55
|
+
install_system_packages
|
56
|
+
end
|
57
|
+
|
58
|
+
def uninstall_python
|
59
|
+
uninstall_system_packages
|
60
|
+
end
|
61
|
+
|
62
|
+
def system_package_candidates(version)
|
63
|
+
[].tap do |names|
|
64
|
+
# For two (or more) digit versions.
|
65
|
+
if match = version.match(/^(\d+\.\d+)/)
|
66
|
+
# Debian style pythonx.y
|
67
|
+
names << "python#{match[1]}"
|
68
|
+
# Amazon style pythonxy
|
69
|
+
names << "python#{match[1].gsub(/\./, '')}"
|
70
|
+
end
|
71
|
+
# Aliases for 2 and 3.
|
72
|
+
if version == '3' || version == ''
|
73
|
+
names.concat(%w{python3.5 python35 python3.4 python34 python3.3 python33 python3.2 python32 python3.1 python31 python3.0 python30 python3})
|
74
|
+
end
|
75
|
+
if version == '2' || version == ''
|
76
|
+
names.concat(%w{python2.7 python27 python2.6 python26 python2.5 python25})
|
77
|
+
end
|
78
|
+
# For RHEL and friends.
|
79
|
+
names << 'python'
|
80
|
+
names.uniq!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
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 'poise_python/resources/pip_requirements'
|
18
|
+
require 'poise_python/resources/python_package'
|
19
|
+
require 'poise_python/resources/python_runtime'
|
20
|
+
require 'poise_python/resources/python_runtime_pip'
|
21
|
+
require 'poise_python/resources/python_execute'
|
22
|
+
require 'poise_python/resources/python_virtualenv'
|
23
|
+
|
24
|
+
|
25
|
+
module PoisePython
|
26
|
+
# Chef resources and providers for poise-python.
|
27
|
+
#
|
28
|
+
# @since 1.0.0
|
29
|
+
module Resources
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
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/provider'
|
18
|
+
require 'chef/resource'
|
19
|
+
require 'poise'
|
20
|
+
|
21
|
+
require 'poise_python/python_command_mixin'
|
22
|
+
|
23
|
+
|
24
|
+
module PoisePython
|
25
|
+
module Resources
|
26
|
+
# (see PipRequirements::Resource)
|
27
|
+
# @since 1.0.0
|
28
|
+
module PipRequirements
|
29
|
+
# A `pip_requirements` resource to manage Python virtual environments.
|
30
|
+
#
|
31
|
+
# @provides pip_requirements
|
32
|
+
# @action install
|
33
|
+
# @action upgrade
|
34
|
+
# @example
|
35
|
+
# pip_requirements '/opt/myapp/requirements.txt'
|
36
|
+
class Resource < Chef::Resource
|
37
|
+
include PoisePython::PythonCommandMixin
|
38
|
+
provides(:pip_requirements)
|
39
|
+
actions(:install, :upgrade)
|
40
|
+
|
41
|
+
# @!attribute path
|
42
|
+
# Path to the requirements file, or a folder containing the
|
43
|
+
# requirements file.
|
44
|
+
# @return [String]
|
45
|
+
attribute(:path, kind_of: String, name_attribute: true)
|
46
|
+
end
|
47
|
+
|
48
|
+
# The default provider for `pip_requirements`.
|
49
|
+
#
|
50
|
+
# @see Resource
|
51
|
+
# @provides pip_requirements
|
52
|
+
class Provider < Chef::Provider
|
53
|
+
include Poise
|
54
|
+
include PoisePython::PythonCommandMixin
|
55
|
+
provides(:pip_requirements)
|
56
|
+
|
57
|
+
# The `install` action for the `pip_requirements` resource.
|
58
|
+
#
|
59
|
+
# @return [void]
|
60
|
+
def action_install
|
61
|
+
install_requirements(upgrade: false)
|
62
|
+
end
|
63
|
+
|
64
|
+
# The `upgrade` action for the `pip_requirements` resource.
|
65
|
+
#
|
66
|
+
# @return [void]
|
67
|
+
def action_upgrade
|
68
|
+
install_requirements(upgrade: true)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Run an install --requirements command and parse the output.
|
74
|
+
#
|
75
|
+
# @param upgrade [Boolean] If we should use the --upgrade flag.
|
76
|
+
# @return [void]
|
77
|
+
def install_requirements(upgrade: false)
|
78
|
+
cmd = %w{-m pip.__main__ install}
|
79
|
+
cmd << '--upgrade' if upgrade
|
80
|
+
cmd << '--requirement'
|
81
|
+
cmd << requirements_path
|
82
|
+
output = python_shell_out!(cmd).stdout
|
83
|
+
if output.include?('Successfully installed')
|
84
|
+
new_resource.updated_by_last_action(true)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Find the true path to the requirements file.
|
89
|
+
#
|
90
|
+
# @return [String]
|
91
|
+
def requirements_path
|
92
|
+
if ::File.directory?(new_resource.path)
|
93
|
+
::File.join(new_resource.path, 'requirements.txt')
|
94
|
+
else
|
95
|
+
new_resource.path
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
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/mixin/which'
|
18
|
+
require 'chef/provider/execute'
|
19
|
+
require 'chef/resource/execute'
|
20
|
+
require 'poise'
|
21
|
+
|
22
|
+
require 'poise_python/python_command_mixin'
|
23
|
+
|
24
|
+
|
25
|
+
module PoisePython
|
26
|
+
module Resources
|
27
|
+
# (see PythonExecute::Resource)
|
28
|
+
# @since 1.0.0
|
29
|
+
module PythonExecute
|
30
|
+
# A `python_execute` resource to run Python scripts and commands.
|
31
|
+
#
|
32
|
+
# @provides python_execute
|
33
|
+
# @action run
|
34
|
+
# @example
|
35
|
+
# python_execute 'myapp.py' do
|
36
|
+
# user 'myuser'
|
37
|
+
# end
|
38
|
+
class Resource < Chef::Resource::Execute
|
39
|
+
include PoisePython::PythonCommandMixin
|
40
|
+
provides(:python_execute)
|
41
|
+
actions(:run)
|
42
|
+
end
|
43
|
+
|
44
|
+
# The default provider for `python_execute`.
|
45
|
+
#
|
46
|
+
# @see Resource
|
47
|
+
# @provides python_execute
|
48
|
+
class Provider < Chef::Provider::Execute
|
49
|
+
include Chef::Mixin::Which
|
50
|
+
provides(:python_execute)
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Command to pass to shell_out.
|
55
|
+
#
|
56
|
+
# @return [String, Array<String>]
|
57
|
+
def command
|
58
|
+
if new_resource.command.is_a?(Array)
|
59
|
+
[new_resource.python] + new_resource.command
|
60
|
+
else
|
61
|
+
"#{new_resource.python} #{new_resource.command}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Environment variables to pass to shell_out.
|
66
|
+
#
|
67
|
+
# @return [Hash]
|
68
|
+
def environment
|
69
|
+
if new_resource.parent_python
|
70
|
+
environment = new_resource.parent_python.python_environment
|
71
|
+
if new_resource.environment
|
72
|
+
environment = environment.merge(new_resource.environment)
|
73
|
+
end
|
74
|
+
environment
|
75
|
+
else
|
76
|
+
new_resource.environment
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,322 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2015, Noah Kantrowitz
|
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 'shellwords'
|
18
|
+
|
19
|
+
require 'chef/mixin/which'
|
20
|
+
require 'chef/provider/package'
|
21
|
+
require 'chef/resource/package'
|
22
|
+
require 'poise'
|
23
|
+
|
24
|
+
require 'poise_python/python_command_mixin'
|
25
|
+
|
26
|
+
|
27
|
+
module PoisePython
|
28
|
+
module Resources
|
29
|
+
# (see PythonPackage::Resource)
|
30
|
+
# @since 1.0.0
|
31
|
+
module PythonPackage
|
32
|
+
# A Python snippet to hack pip a bit so `pip list --outdated` will show
|
33
|
+
# only the things we want and will understand version requirements.
|
34
|
+
# @api private
|
35
|
+
PIP_HACK_SCRIPT = <<-EOH
|
36
|
+
import sys
|
37
|
+
|
38
|
+
import pip
|
39
|
+
try:
|
40
|
+
# >= 6.0
|
41
|
+
from pip.utils import get_installed_distributions
|
42
|
+
except ImportError:
|
43
|
+
# <= 1.5.6
|
44
|
+
from pip.util import get_installed_distributions
|
45
|
+
|
46
|
+
def replacement(*args, **kwargs):
|
47
|
+
import copy, sys
|
48
|
+
from pip._vendor import pkg_resources
|
49
|
+
dists = []
|
50
|
+
for raw_req in sys.argv[3:]:
|
51
|
+
req = pkg_resources.Requirement.parse(raw_req)
|
52
|
+
dist = pkg_resources.working_set.by_key.get(req.key)
|
53
|
+
if dist:
|
54
|
+
# Don't mutate stuff from the global working set.
|
55
|
+
dist = copy.copy(dist)
|
56
|
+
else:
|
57
|
+
# Make a fake one.
|
58
|
+
dist = pkg_resources.Distribution(project_name=req.key, version='0')
|
59
|
+
# Fool the .key property into using our string.
|
60
|
+
dist._key = raw_req
|
61
|
+
dists.append(dist)
|
62
|
+
return dists
|
63
|
+
try:
|
64
|
+
# For Python 2.
|
65
|
+
get_installed_distributions.func_code = replacement.func_code
|
66
|
+
except AttributeError:
|
67
|
+
# For Python 3.
|
68
|
+
get_installed_distributions.__code__ = replacement.__code__
|
69
|
+
|
70
|
+
sys.exit(pip.main())
|
71
|
+
EOH
|
72
|
+
|
73
|
+
# A `python_package` resource to manage Python installations using pip.
|
74
|
+
#
|
75
|
+
# @provides python_package
|
76
|
+
# @action install
|
77
|
+
# @action upgrade
|
78
|
+
# @action uninstall
|
79
|
+
# @example
|
80
|
+
# python_package 'django' do
|
81
|
+
# python '2'
|
82
|
+
# version '1.8.3'
|
83
|
+
# end
|
84
|
+
class Resource < Chef::Resource::Package
|
85
|
+
include PoisePython::PythonCommandMixin
|
86
|
+
provides(:python_package)
|
87
|
+
|
88
|
+
|
89
|
+
# @!attribute group
|
90
|
+
# System group to install the package.
|
91
|
+
# @return [String, Integer, nil]
|
92
|
+
attribute(:group, kind_of: [String, Integer, NilClass])
|
93
|
+
# @!attribute user
|
94
|
+
# System user to install the package.
|
95
|
+
# @return [String, Integer, nil]
|
96
|
+
attribute(:user, kind_of: [String, Integer, NilClass])
|
97
|
+
|
98
|
+
def initialize(*args)
|
99
|
+
super
|
100
|
+
# For older Chef.
|
101
|
+
@resource_name = :python_package
|
102
|
+
# We don't have these actions.
|
103
|
+
@allowed_actions.delete(:purge)
|
104
|
+
@allowed_actions.delete(:reconfig)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Upstream attribute we don't support. Sets are an error and gets always
|
108
|
+
# return nil.
|
109
|
+
#
|
110
|
+
# @api private
|
111
|
+
# @param arg [Object] Ignored
|
112
|
+
# @return [nil]
|
113
|
+
def response_file(arg=nil)
|
114
|
+
raise NoMethodError if arg
|
115
|
+
end
|
116
|
+
|
117
|
+
# (see #response_file)
|
118
|
+
def response_file_variables(arg=nil)
|
119
|
+
raise NoMethodError if arg
|
120
|
+
end
|
121
|
+
|
122
|
+
# (see #response_file)
|
123
|
+
def source(arg=nil)
|
124
|
+
raise NoMethodError if arg
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# The default provider for the `python_package` resource.
|
129
|
+
#
|
130
|
+
# @see Resource
|
131
|
+
class Provider < Chef::Provider::Package
|
132
|
+
include PoisePython::PythonCommandMixin
|
133
|
+
provides(:python_package)
|
134
|
+
|
135
|
+
# Load current and candidate versions for all needed packages.
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
# @return [Chef::Resource]
|
139
|
+
def load_current_resource
|
140
|
+
@current_resource = new_resource.class.new(new_resource.name, run_context)
|
141
|
+
current_resource.package_name(new_resource.package_name)
|
142
|
+
check_package_versions(current_resource)
|
143
|
+
current_resource
|
144
|
+
end
|
145
|
+
|
146
|
+
# Populate current and candidate versions for all needed packages.
|
147
|
+
#
|
148
|
+
# @api private
|
149
|
+
# @param resource [PoisePython::Resources::PythonPackage::Resource]
|
150
|
+
# Resource to load for.
|
151
|
+
# @param version [String, Array<String>] Current version(s) of package(s).
|
152
|
+
# @return [void]
|
153
|
+
def check_package_versions(resource, version=new_resource.version)
|
154
|
+
version_data = Hash.new {|hash, key| hash[key] = {current: nil, candidate: nil} }
|
155
|
+
# Get the version for everything currently installed.
|
156
|
+
list = pip_command('list').stdout
|
157
|
+
parse_pip_list(list).each do |name, current|
|
158
|
+
# Merge current versions in to the data.
|
159
|
+
version_data[name][:current] = current
|
160
|
+
end
|
161
|
+
# Check for newer candidates.
|
162
|
+
outdated = pip_outdated(pip_requirements(resource.package_name, version)).stdout
|
163
|
+
parse_pip_outdated(outdated).each do |name, candidate|
|
164
|
+
# Merge candidates in to the existing versions.
|
165
|
+
version_data[name][:candidate] = candidate
|
166
|
+
end
|
167
|
+
# Populate the current resource and candidate versions. Youch this is
|
168
|
+
# a gross mix of data flow.
|
169
|
+
if(resource.package_name.is_a?(Array))
|
170
|
+
@candidate_version = []
|
171
|
+
versions = []
|
172
|
+
[resource.package_name].flatten.each do |name|
|
173
|
+
ver = version_data[name.downcase]
|
174
|
+
versions << ver[:current]
|
175
|
+
@candidate_version << ver[:candidate]
|
176
|
+
end
|
177
|
+
resource.version(versions)
|
178
|
+
else
|
179
|
+
ver = version_data[resource.package_name.downcase]
|
180
|
+
resource.version(ver[:current])
|
181
|
+
@candidate_version = ver[:candidate]
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Install package(s) using pip.
|
186
|
+
#
|
187
|
+
# @param name [String, Array<String>] Name(s) of package(s).
|
188
|
+
# @param version [String, Array<String>] Version(s) of package(s).
|
189
|
+
# @return [void]
|
190
|
+
def install_package(name, version)
|
191
|
+
pip_install(name, version, upgrade: false)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Upgrade package(s) using pip.
|
195
|
+
#
|
196
|
+
# @param name [String, Array<String>] Name(s) of package(s).
|
197
|
+
# @param version [String, Array<String>] Version(s) of package(s).
|
198
|
+
# @return [void]
|
199
|
+
def upgrade_package(name, version)
|
200
|
+
pip_install(name, version, upgrade: true)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Uninstall package(s) using pip.
|
204
|
+
#
|
205
|
+
# @param name [String, Array<String>] Name(s) of package(s).
|
206
|
+
# @param version [String, Array<String>] Version(s) of package(s).
|
207
|
+
# @return [void]
|
208
|
+
def remove_package(name, version)
|
209
|
+
pip_command('uninstall', %w{--yes} + [name].flatten)
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
# Convert name(s) and version(s) to an array of pkg_resources.Requirement
|
215
|
+
# compatible strings. These are strings like "django" or "django==1.0".
|
216
|
+
#
|
217
|
+
# @param name [String, Array<String>] Name or names for the packages.
|
218
|
+
# @param version [String, Array<String>] Version or versions for the
|
219
|
+
# packages.
|
220
|
+
# @return [Array<String>]
|
221
|
+
def pip_requirements(name, version)
|
222
|
+
[name].flatten.zip([version].flatten).map do |n, v|
|
223
|
+
v = v.to_s.strip
|
224
|
+
if v.empty?
|
225
|
+
# No version requirement, send through unmodified.
|
226
|
+
n
|
227
|
+
elsif v =~ /^\d/
|
228
|
+
"#{n}==#{v}"
|
229
|
+
else
|
230
|
+
# If the first character isn't a digit, assume something fancy.
|
231
|
+
n + v
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Run a pip command.
|
237
|
+
#
|
238
|
+
# @param pip_command [String] The pip subcommand to run (eg. install).
|
239
|
+
# @param pip_options [Array<String>] Options for the pip command.
|
240
|
+
# @param opts [Hash] Mixlib::ShellOut options.
|
241
|
+
# @return [Mixlib::ShellOut]
|
242
|
+
def pip_command(pip_command, pip_options=[], opts={})
|
243
|
+
runner = opts.delete(:pip_runner) || %w{-m pip.__main__}
|
244
|
+
full_cmd = if new_resource.options
|
245
|
+
# We have to use a string for this case to be safe because the
|
246
|
+
# options are a string and I don't want to try and parse that.
|
247
|
+
"##{runner.join(' ')} #{pip_command} #{new_resource.options} #{Shellwords.join(pip_options)}"
|
248
|
+
else
|
249
|
+
# No special options, use an array to skip the extra /bin/sh.
|
250
|
+
runner + [pip_command] + pip_options
|
251
|
+
end
|
252
|
+
# Set user and group.
|
253
|
+
opts[:user] = new_resource.user if new_resource.user
|
254
|
+
opts[:group] = new_resource.group if new_resource.group
|
255
|
+
|
256
|
+
python_shell_out!(full_cmd, opts)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Run `pip install` to install a package(s).
|
260
|
+
#
|
261
|
+
# @param name [String, Array<String>] Name(s) of package(s) to install.
|
262
|
+
# @param version [String, Array<String>] Version(s) of package(s) to
|
263
|
+
# install.
|
264
|
+
# @param upgrade [Boolean] Use upgrade mode?
|
265
|
+
# @return [Mixlib::ShellOut]
|
266
|
+
def pip_install(name, version, upgrade: false)
|
267
|
+
cmd = pip_requirements(name, version)
|
268
|
+
# Prepend --upgrade if needed.
|
269
|
+
cmd = %w{--upgrade} + cmd if upgrade
|
270
|
+
pip_command('install', cmd)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Run my hacked version of `pip list --outdated` with a specific set of
|
274
|
+
# package requirements.
|
275
|
+
#
|
276
|
+
# @see #pip_requirements
|
277
|
+
# @param requirements [Array<String>] Pip-formatted package requirements.
|
278
|
+
# @return [Mixlib::ShellOut]
|
279
|
+
def pip_outdated(requirements)
|
280
|
+
pip_command('list', %w{--outdated} + requirements, input: PIP_HACK_SCRIPT, pip_runner: %w{-})
|
281
|
+
end
|
282
|
+
|
283
|
+
# Parse the output from `pip list --outdate`. Returns a hash of package
|
284
|
+
# key to candidate version.
|
285
|
+
#
|
286
|
+
# @param text [String] Output to parse.
|
287
|
+
# @return [Hash<String, String>]
|
288
|
+
def parse_pip_outdated(text)
|
289
|
+
text.split(/\n/).inject({}) do |memo, line|
|
290
|
+
# Example of a line:
|
291
|
+
# boto (Current: 2.25.0 Latest: 2.38.0 [wheel])
|
292
|
+
if md = line.match(/^(\S+)\s+\(.*?latest:\s+([^\s,]+).*\)$/i)
|
293
|
+
memo[md[1].downcase] = md[2]
|
294
|
+
else
|
295
|
+
Chef::Log.debug("[#{new_resource}] Unparsable line in pip outdated: #{line}")
|
296
|
+
end
|
297
|
+
memo
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Parse the output from `pip list`. Returns a hash of package key to
|
302
|
+
# current version.
|
303
|
+
#
|
304
|
+
# @param text [String] Output to parse.
|
305
|
+
# @return [Hash<String, String>]
|
306
|
+
def parse_pip_list(text)
|
307
|
+
text.split(/\n/).inject({}) do |memo, line|
|
308
|
+
# Example of a line:
|
309
|
+
# boto (2.25.0)
|
310
|
+
if md = line.match(/^(\S+)\s+\(([^\s,]+).*\)$/i)
|
311
|
+
memo[md[1].downcase] = md[2]
|
312
|
+
else
|
313
|
+
Chef::Log.debug("[#{new_resource}] Unparsable line in pip list: #{line}")
|
314
|
+
end
|
315
|
+
memo
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|