diff --git a/.fixtures.yml b/.fixtures.yml new file mode 100644 index 00000000..33c14915 --- /dev/null +++ b/.fixtures.yml @@ -0,0 +1,5 @@ +fixtures: + repositories: + "stdlib": "git://github.com/puppetlabs/puppetlabs-stdlib.git" + symlinks: + "python": "#{source_dir}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3dbc3019 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Editors +project.xml +project.properties +/nbproject/private/ +.buildpath +.project +.settings* +sftp-config.json +.idea + +# OS X metadata +.DS_Store + +# Windows junk +Thumbs.db + +# Modules +pkg/ +Gemfile.lock +vendor/ +spec/fixtures/ +.bundle/ +.vagrant/ +coverage/ + +# Beaker +.vagrant/ +log/ +junit/ \ No newline at end of file diff --git a/.sync.yml b/.sync.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.sync.yml @@ -0,0 +1 @@ +--- diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ff985b98 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +--- +language: ruby +bundler_args: --without system_tests +script: "bundle exec rake validate && bundle exec rake lint && bundle exec rake spec SPEC_OPTS='--format documentation'" +matrix: + fast_finish: true + include: + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.6.0" + - rvm: 1.8.7 + env: PUPPET_GEM_VERSION="~> 2.7.0" FACTER_GEM_VERSION="~> 1.7.0" + - rvm: 1.9.3 + env: PUPPET_GEM_VERSION="~> 3.0" + - rvm: 2.0.0 + env: PUPPET_GEM_VERSION="~> 3.0" \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..f2f1b41c --- /dev/null +++ b/Gemfile @@ -0,0 +1,31 @@ +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +group :development, :test do + gem 'rake', :require => false + gem 'rspec-puppet', :require => false + gem 'puppetlabs_spec_helper', :require => false + gem 'puppet-lint', :require => false + gem 'pry', :require => false + gem 'simplecov', :require => false + gem 'rspec', '~> 2.99' +end + +if facterversion = ENV['FACTER_GEM_VERSION'] + gem 'facter', facterversion, :require => false +else + gem 'facter', :require => false +end + +if puppetversion = ENV['PUPPET_GEM_VERSION'] + gem 'puppet', puppetversion, :require => false +else + gem 'puppet', :require => false +end + +group :system_tests do + gem 'serverspec', :require => false + gem 'beaker', :require => false + gem 'beaker-rspec', :require => false +end + +# vim:ft=ruby diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..404e08ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © [2012-2014] [Sergey Stankevich] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 048a291d..00000000 --- a/LICENSE.md +++ /dev/null @@ -1,13 +0,0 @@ -Copyright © 2012 Sergey Stankevich - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -    [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/Modulefile b/Modulefile index c7f2f97f..e92b7960 100644 --- a/Modulefile +++ b/Modulefile @@ -1,9 +1,11 @@ -name 'puppet-python' -version '1.0.0' +name 'stankevich-python' +version '1.9.1' +source 'git://github.com/stankevich/puppet-python.git' +author 'stankevich' +license 'Apache License, Version 2.0' +summary 'Python Module' +description 'Puppet module for Python' +project_page 'https://github.com/stankevich/puppet-python' -author 'Sergey Stankevich' -license 'Apache License, Version 2.0' -project_page 'https://github.com/stankevich/puppet-python' -source 'git://github.com/stankevich/puppet-python' -summary 'Puppet module for Python' -description 'Installs and manages Python, pip, virtualenv, Gunicorn virtual hosts.' +## Add dependencies, if any: +dependency 'puppetlabs/stdlib', '>= 4.0.0' diff --git a/README.md b/README.md index a7b00d5d..e850e9a3 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,285 @@ -[puppet-python](https://github.com/stankevich/puppet-python) -====== +# puppet-python [![Build Status](https://travis-ci.org/stankevich/puppet-python.svg?branch=master)](https://travis-ci.org/stankevich/puppet-python) -Puppet module for installing and managing python, pip, virtualenv, Gunicorn virtual hosts +Puppet module for installing and managing python, pip, virtualenvs and Gunicorn virtual hosts. + +**Version 1.1.x Notes** + +Version `1.1.x` makes several fundamental changes to the core of this module, adding some additional features, improving performance and making operations more robust in general. + +Please note that several changes have been made in `v1.1.x` which make manifests incompatible with the previous version. However, modifying your manifests to suit is trivial. Please see the notes below. + +Currently, the changes you need to make are as follows: + +* All pip definitions MUST include the owner field which specifies which user owns the virtualenv that packages will be installed in. Adding this greatly improves performance and efficiency of this module. +* You must explicitly specify pip => true in the python class if you want pip installed. As such, the pip package is now independent of the dev package and so one can exist without the other. + +## Installation + +```shell +git submodule add https://github.com/stankevich/puppet-python.git /path/to/python +``` +OR + +``` shell +puppet module install stankevich-python +``` ## Usage ### python -Installs and manages python, python-dev, python-virtualenv and Gunicorn. +Installs and manages python, python-pip, python-dev, python-virtualenv and Gunicorn. + +**version** - Python version to install. Default: system default -**version** — Python version to install. Default: system default +**pip** - Install python-pip. Default: true -**dev** — Install python-dev. Default: false +**dev** - Install python-dev. Default: false -**virtualenv** — Install python-virtualenv. Default: false +**virtualenv** - Install python-virtualenv. Default: false -**gunicorn** — Install Gunicorn. Default: false +**gunicorn** - Install Gunicorn. Default: false - class { 'python': - version => 'system', - dev => true, - virtualenv => true, - gunicorn => true, - } +**manage_gunicorn** - Allow Installation / Removal of Gunicorn. Default: true + +```puppet + class { 'python' : + version => 'system', + pip => true, + dev => true, + virtualenv => true, + gunicorn => true, + } +``` ### python::pip Installs and manages packages from pip. -**ensure** — present/absent. Default: present +**pkgname** - the name of the package to install. Required. + +**ensure** - present/latest/absent. You can also specify the version. Default: present + +**virtualenv** - virtualenv to run pip in. Default: system (no virtualenv) + +**url** - URL to install from. Default: none + +**owner** - The owner of the virtualenv to ensure that packages are installed with the correct permissions (must be specified). Default: root -**virtualenv** — virtualenv to run pip in. +**proxy** - Proxy server to use for outbound connections. Default: none -**proxy** — Proxy server to use for outbound connections. Default: none +**environment** - Additional environment variables required to install the packages. Default: none - python::pip { 'flask': - virtualenv => '/var/www/project1', - proxy => 'http://proxy.domain.com:3128', - } +**egg** - The egg name to use. Default: `$name` of the class, e.g. cx_Oracle + +**install_args** - Array of additional flags to pass to pip during installaton. Default: none + +**uninstall_args** - Array of additional flags to pass to pip during uninstall. Default: none + +**timeout** - Timeout for the pip install command. Defaults to 1800. +```puppet + python::pip { 'cx_Oracle' : + pkgname => 'cx_Oracle', + ensure => '5.1.2', + virtualenv => '/var/www/project1', + owner => 'appuser', + proxy => 'http://proxy.domain.com:3128', + environment => 'ORACLE_HOME=/usr/lib/oracle/11.2/client64', + install_args => ['-e'], + timeout => 1800, + } +``` ### python::requirements Installs and manages Python packages from requirements file. -**virtualenv** — virtualenv to run pip in. Default: system-wide +**virtualenv** - virtualenv to run pip in. Default: system-wide + +**proxy** - Proxy server to use for outbound connections. Default: none -**proxy** — Proxy server to use for outbound connections. Default: none +**owner** - The owner of the virtualenv to ensure that packages are installed with the correct permissions (must be specified). Default: root - python::requirements { '/var/www/project1/requirements.txt': - virtualenv => '/var/www/project1', - proxy => 'http://proxy.domain.com:3128', - } +**src** - The `--src` parameter to `pip`, used to specify where to install `--editable` resources; by default no `--src` parameter is passed to `pip`. + +**group** - The group that was used to create the virtualenv. This is used to create the requirements file with correct permissions if it's not present already. + +```puppet + python::requirements { '/var/www/project1/requirements.txt' : + virtualenv => '/var/www/project1', + proxy => 'http://proxy.domain.com:3128', + owner => 'appuser', + group => 'apps', + } +``` ### python::virtualenv Creates Python virtualenv. -**ensure** — present/absent. Default: present +**ensure** - present/absent. Default: present + +**version** - Python version to use. Default: system default + +**requirements** - Path to pip requirements.txt file. Default: none + +**proxy** - Proxy server to use for outbound connections. Default: none + +**systempkgs** - Copy system site-packages into virtualenv. Default: don't + +**distribute** - Include distribute in the virtualenv. Default: true + +**venv_dir** - The location of the virtualenv if resource path not specified. Must be absolute path. Default: resource name + +**owner** - Specify the owner of this virtualenv + +**group** - Specify the group for this virtualenv -**version** — Python version to use. Default: system default +**index** - Base URL of Python package index. Default: none -**requirements** — Path to pip requirements.txt file. Default: none +**cwd** - The directory from which to run the "pip install" command. Default: undef -**proxy** — Proxy server to use for outbound connections. Default: none +**timeout** - The maximum time in seconds the "pip install" command should take. Default: 1800 - python::virtualenv { '/var/www/project1': - ensure => present, - version => 'system', - requirements => '/var/www/project1/requirements.txt', - proxy => 'http://proxy.domain.com:3128', - } +```puppet + python::virtualenv { '/var/www/project1' : + ensure => present, + version => 'system', + requirements => '/var/www/project1/requirements.txt', + proxy => 'http://proxy.domain.com:3128', + systempkgs => true, + distribute => false, + venv_dir => '/home/appuser/virtualenvs', + owner => 'appuser', + group => 'apps', + cwd => '/var/www/project1', + timeout => 0, + } +``` + +### python::pyvenv + +Creates Python3 virtualenv. + +**ensure** - present/absent. Default: present + +**version** - Python version to use. Default: system default + +**systempkgs** - Copy system site-packages into virtualenv. Default: don't + +**venv_dir** - The location of the virtualenv if resource path not specified. Must be absolute path. Default: resource name + +**owner** - Specify the owner of this virtualenv + +**group** - Specify the group for this virtualenv + +**path** - Specify the path that contains the pyvenv executable. Default: /bin/, /usr/bin, /usr/sbin + +**environment** - Specify any environment variables to use when creating pyvenv + +```puppet + python::pyvenv { '/var/www/project1' : + ensure => present, + version => 'system', + systempkgs => true, + venv_dir => '/home/appuser/virtualenvs', + owner => 'appuser', + group => 'apps', + } +``` ### python::gunicorn Manages Gunicorn virtual hosts. -**ensure** — present/absent. Default: present +**ensure** - present/absent. Default: present + +**virtualenv** - Run in virtualenv, specify directory. Default: disabled + +**mode** - Gunicorn mode. wsgi/django. Default: wsgi + +**dir** - Application directory. + +**bind** - Bind on: 'HOST', 'HOST:PORT', 'unix:PATH'. Default: `unix:/tmp/gunicorn-$name.socket` or `unix:${virtualenv}/${name}.socket` + +**environment** - Set ENVIRONMENT variable. Default: none + +**appmodule** - Set the application module name for gunicorn to load when not using Django. Default: `app:app` + +**osenv** - Allows setting environment variables for the gunicorn service. Accepts a hash of 'key': 'value' pairs. Default: false + +**timeout** - Allows setting the gunicorn idle worker process time before being killed. The unit of time is seconds. Default: 30 + +**template** - Which ERB template to use. Default: python/gunicorn.erb + +```puppet + python::gunicorn { 'vhost' : + ensure => present, + virtualenv => '/var/www/project1', + mode => 'wsgi', + dir => '/var/www/project1/current', + bind => 'unix:/tmp/gunicorn.socket', + environment => 'prod', + appmodule => 'app:app', + osenv => { 'DBHOST' => 'dbserver.example.com' }, + timeout => 30, + template => 'python/gunicorn.erb', + } +``` + +### python::dotfile + +Manages arbitrary python dotiles with a simple config hash. + +**ensure** - present/absent. Default: present + +**filename** - Default: $title + +**mode** - Default: 0644 + +**owner** - Default: root -**virtualenv** — Run in virtualenv, specify directory. Default: disabled +**group** - Default: root -**mode** — Gunicorn mode. wsgi/django. Default: wsgi +**config** Config hash. This will be expanded to an ini-file. Default: {} -**dir** — Application directory. +```puppet +python::dotfile { '/var/lib/jenkins/.pip/pip.conf': + ensure => present, + owner => 'jenkins', + group => 'jenkins', + config => { + 'global' => { + 'index-url => 'https://mypypi.acme.com/simple/' + 'extra-index-url => https://pypi.risedev.at/simple/ + } + } +} +``` -**bind** — Bind on: 'HOST', 'HOST:PORT', 'unix:PATH'. Default: unix:/tmp/gunicorn-$name.socket or unix:${virtualenv}/${name}.socket +### hiera configuration -**environment** — Set ENVIRONMENT variable. Default: none +This module supports configuration through hiera. The following example +creates two python3 virtualenvs. The configuration also pip installs a +package into each environment. - python::gunicorn { 'vhost': - ensure => present, - virtualenv => '/var/www/project1', - mode => 'wsgi', - dir => '/var/www/project1/current', - bind => 'unix:/tmp/gunicorn.socket', - environment => 'prod', - } +```yaml +python::python_pyvenvs: + "/opt/env1": + version: "system" + "/opt/env2": + version: "system" +python::python_pips: + "nose": + virtualenv: "/opt/env1" + "coverage": + virtualenv: "/opt/env2" +``` ## Authors [Sergey Stankevich](https://github.com/stankevich) +[Shiva Poudel](https://github.com/shivapoudel) +[Ashley Penney](https://github.com/apenney) +[Marc Fournier](https://github.com/mfournier) +[Fotis Gimian](https://github.com/fgimian) diff --git a/Rakefile b/Rakefile index 58df3ec3..1f6c81f0 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,39 @@ -# Rakefile for puppet-lint (https://github.com/rodjek/puppet-lint) -# Run: rake lint - +require 'puppetlabs_spec_helper/rake_tasks' require 'puppet-lint/tasks/puppet-lint' -PuppetLint.configuration.with_filename = true -PuppetLint.configuration.send('disable_documentation') -PuppetLint.configuration.send('disable_class_parameter_defaults') +require 'puppet-syntax/tasks/puppet-syntax' + +# These two gems aren't always present, for instance +# on Travis with --without development +begin + require 'puppet_blacksmith/rake_tasks' +rescue LoadError +end + +PuppetLint.configuration.fail_on_warnings +PuppetLint.configuration.send('relative') PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.send('disable_class_inherits_from_params_class') +PuppetLint.configuration.send('disable_class_parameter_defaults') +PuppetLint.configuration.send('disable_documentation') +PuppetLint.configuration.send('disable_single_quote_string_with_variables') +PuppetLint.configuration.ignore_paths = ["spec/**/*.pp", "pkg/**/*.pp"] + +exclude_paths = [ + "pkg/**/*", + "vendor/**/*", + "spec/**/*", +] +PuppetLint.configuration.ignore_paths = exclude_paths +PuppetSyntax.exclude_paths = exclude_paths + +desc "Run acceptance tests" +RSpec::Core::RakeTask.new(:acceptance) do |t| + t.pattern = 'spec/acceptance' +end + +desc "Run syntax, lint, and spec tests." +task :test => [ + :syntax, + :lint, + :spec, +] \ No newline at end of file diff --git a/lib/facter/pip_version.rb b/lib/facter/pip_version.rb new file mode 100644 index 00000000..70b5f9fa --- /dev/null +++ b/lib/facter/pip_version.rb @@ -0,0 +1,33 @@ +# Make pip version available as a fact +# Works with pip loaded and without, pip installed using pip and package installed + +facter_puppet_version = Facter.value(:puppetversion) +facter_is_pe = Facter.value(:is_pe) + +if facter_is_pe + facter_puppet_version = facter_puppet_version.to_s.split(' ')[0] +end + +if (Puppet::Util::Package.versioncmp(facter_puppet_version, '3.6') >= 0) + pkg = Puppet::Type.type(:package).new(:name => 'python-pip', :allow_virtual => 'false') +else + pkg = Puppet::Type.type(:package).new(:name => 'python-pip') +end + +Facter.add("pip_version") do + has_weight 100 + setcode do + if Facter::Util::Resolution.which('pip') + Facter::Util::Resolution.exec('pip --version 2>&1').match(/^pip (\d+\.\d+\.?\d*).*$/)[1] + end + end +end + +Facter.add("pip_version") do + has_weight 50 + setcode do + unless [:absent,:purged].include?(pkg.retrieve[pkg.property(:ensure)]) + pkg.retrieve[pkg.property(:ensure)] + end + end +end diff --git a/lib/facter/python_version.rb b/lib/facter/python_version.rb new file mode 100644 index 00000000..febcd81e --- /dev/null +++ b/lib/facter/python_version.rb @@ -0,0 +1,41 @@ +# Make python versions available as facts +# In lists default python and system python versions + +facter_puppet_version = Facter.value(:puppetversion) +facter_is_pe = Facter.value(:is_pe) + +if facter_is_pe + facter_puppet_version = facter_puppet_version.to_s.split(' ')[0] +end + +if (Puppet::Util::Package.versioncmp(facter_puppet_version, '3.6') >= 0) + pkg = Puppet::Type.type(:package).new(:name => 'python-pip', :allow_virtual => 'false') +else + pkg = Puppet::Type.type(:package).new(:name => 'python-pip') +end + +Facter.add("system_python_version") do + setcode do + unless [:absent,:purged].include?(pkg.retrieve[pkg.property(:ensure)]) + pkg.retrieve[pkg.property(:ensure)] + end + end +end + +Facter.add("python_version") do + has_weight 100 + setcode do + if Facter::Util::Resolution.which('python') + Facter::Util::Resolution.exec('python -V 2>&1').match(/^.*(\d+\.\d+\.\d+)$/)[1] + end + end +end + +Facter.add("python_version") do + has_weight 50 + setcode do + unless [:absent,:purged].include?(pkg.retrieve[pkg.property(:ensure)]) + pkg.retrieve[pkg.property(:ensure)] + end + end +end diff --git a/lib/facter/virtualenv_version.rb b/lib/facter/virtualenv_version.rb new file mode 100644 index 00000000..d3c1365c --- /dev/null +++ b/lib/facter/virtualenv_version.rb @@ -0,0 +1,33 @@ +# Make virtualenv version available as a fact +# Works with virualenv loaded and without, pip installed and package installed + +facter_puppet_version = Facter.value(:puppetversion) +facter_is_pe = Facter.value(:is_pe) + +if facter_is_pe + facter_puppet_version = facter_puppet_version.to_s.split(' ')[0] +end + +if (Puppet::Util::Package.versioncmp(facter_puppet_version, '3.6') >= 0) + pkg = Puppet::Type.type(:package).new(:name => 'python-pip', :allow_virtual => 'false') +else + pkg = Puppet::Type.type(:package).new(:name => 'python-pip') +end + +Facter.add("virtualenv_version") do + has_weight 100 + setcode do + if Facter::Util::Resolution.which('virtualenv') + Facter::Util::Resolution.exec('virtualenv --version 2>&1').match(/^(\d+\.\d+\.?\d*).*$/)[0] + end + end +end + +Facter.add("virtualenv_version") do + has_weight 50 + setcode do + unless [:absent,:purged].include?(pkg.retrieve[pkg.property(:ensure)]) + pkg.retrieve[pkg.property(:ensure)] + end + end +end diff --git a/manifests/config.pp b/manifests/config.pp index 43da22e4..c7196f2c 100644 --- a/manifests/config.pp +++ b/manifests/config.pp @@ -1,3 +1,18 @@ +# == Define: python::config +# +# Optionally installs the gunicorn service +# +# === Examples +# +# include python::config +# +# === Authors +# +# Sergey Stankevich +# Ashley Penney +# Fotis Gimian +# + class python::config { Class['python::install'] -> Python::Pip <| |> @@ -5,19 +20,20 @@ Class['python::install'] -> Python::Virtualenv <| |> Python::Virtualenv <| |> -> Python::Pip <| |> - Python::Virtualenv <| |> -> Python::Requirements <| |> - if $python::gunicorn { - Class['python::install'] -> Python::Gunicorn <| |> + if $python::manage_gunicorn { + if $python::gunicorn { + Class['python::install'] -> Python::Gunicorn <| |> - Python::Gunicorn <| |> ~> Service['gunicorn'] + Python::Gunicorn <| |> ~> Service['gunicorn'] - service { 'gunicorn': - ensure => running, - enable => true, - hasrestart => true, - hasstatus => false, - pattern => '/usr/bin/gunicorn', + service { 'gunicorn': + ensure => running, + enable => true, + hasrestart => true, + hasstatus => false, + pattern => '/usr/bin/gunicorn', + } } } diff --git a/manifests/dotfile.pp b/manifests/dotfile.pp new file mode 100644 index 00000000..5d5a9c98 --- /dev/null +++ b/manifests/dotfile.pp @@ -0,0 +1,61 @@ +# == Define: python::dotfile +# +# Manages any python dotfiles with a simple config hash. +# +# === Parameters +# +# [*ensure*] +# present|absent. Default: present +# +# [*filename*] +# Filename. Default: $title +# +# [*mode*] +# File mode. Default: 0644 +# +# [*owner*] +# [*group*] +# Owner/group. Default: `root`/`root` +# +# [*config*] +# Config hash. This will be expanded to an ini-file. Default: {} +# +# === Examples +# +# python::dotfile { '/var/lib/jenkins/.pip/pip.conf': +# ensure => present, +# owner => 'jenkins', +# group => 'jenkins', +# config => { +# 'global' => { +# 'index-url => 'https://mypypi.acme.com/simple/' +# 'extra-index-url => https://pypi.risedev.at/simple/ +# } +# } +# } +# +# +define python::dotfile ( + $ensure = 'present', + $filename = $title, + $owner = 'root', + $group = 'root', + $mode = '0644', + $config = {}, +) { + $parent_dir = dirname($filename) + + exec { "create ${title}'s parent dir": + command => "install -o ${owner} -g ${group} -d ${parent_dir}", + path => [ '/usr/bin', '/bin', '/usr/local/bin', ], + creates => $parent_dir, + } + + file { $filename: + ensure => $ensure, + owner => $owner, + group => $group, + content => template("${module_name}/inifile.erb"), + require => Exec["create ${title}'s parent dir"], + } +} diff --git a/manifests/gunicorn.pp b/manifests/gunicorn.pp index 2548e95d..cb0ea4fd 100644 --- a/manifests/gunicorn.pp +++ b/manifests/gunicorn.pp @@ -25,6 +25,23 @@ # [*environment*] # Set ENVIRONMENT variable. Default: none # +# [*appmodule*] +# Set the application module name for gunicorn to load when not using Django. +# Default: app:app +# +# [*osenv*] +# Allows setting environment variables for the gunicorn service. Accepts a +# hash of 'key': 'value' pairs. +# Default: false +# +# [*timeout*] +# Allows setting the gunicorn idle worker process time before being killed. +# The unit of time is seconds. +# Default: 30 +# +# [*template*] +# Which ERB template to use. Default: python/gunicorn.erb +# # === Examples # # python::gunicorn { 'vhost': @@ -34,20 +51,36 @@ # dir => '/var/www/project1/current', # bind => 'unix:/tmp/gunicorn.socket', # environment => 'prod', +# owner => 'www-data', +# group => 'www-data', +# appmodule => 'app:app', +# osenv => { 'DBHOST' => 'dbserver.example.com' }, +# timeout => 30, +# template => 'python/gunicorn.erb', # } # # === Authors # # Sergey Stankevich +# Ashley Penney +# Marc Fournier # define python::gunicorn ( - $ensure = present, - $virtualenv = false, - $mode = 'wsgi', - $dir = false, - $bind = false, - $app_interface = 'wsgi', - $environment = false, + $ensure = present, + $virtualenv = false, + $mode = 'wsgi', + $dir = false, + $bind = false, + $environment = false, + $owner = 'www-data', + $group = 'www-data', + $appmodule = 'app:app', + $osenv = false, + $timeout = 30, + $access_log_format = false, + $accesslog = false, + $errorlog = false, + $template = 'python/gunicorn.erb', ) { # Parameter validation @@ -60,7 +93,7 @@ mode => '0644', owner => 'root', group => 'root', - content => template('python/gunicorn.erb'), + content => template($template), } } diff --git a/manifests/init.pp b/manifests/init.pp index c7ab10f2..464d3782 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -5,21 +5,43 @@ # === Parameters # # [*version*] -# Python version to install. Default: system default +# Python version to install. Beware that valid values for this differ a) by +# the provider you choose and b) by the osfamily/operatingsystem you are using. +# Default: system default +# Allowed values: +# - provider == pip: everything pip allows as a version after the 'python==' +# - else: 'system', 'pypy', 3/3.3/... +# - Be aware that 'system' usually means python 2.X. +# - 'pypy' actually lets us use pypy as python. +# - 3/3.3/... means you are going to install the python3/python3.3/... +# package, if available on your osfamily. +# +# [*pip*] +# Install python-pip. Default: true # # [*dev*] # Install python-dev. Default: false # # [*virtualenv*] -# Install python-virtualenv. Default: false +# Install python-virtualenv. Default: false, also accepts 'pip' which will +# install latest virtualenv from pip rather than package manager # # [*gunicorn*] # Install Gunicorn. Default: false # +# [*manage_gunicorn*] +# Allow Installation / Removal of Gunicorn. Default: true +# +# [*provider*] +# What provider to use for installation of the packages, except gunicorn. +# Default: system default provider +# Allowed values: 'pip' +# # === Examples # # class { 'python': # version => 'system', +# pip => true, # dev => true, # virtualenv => true, # gunicorn => true, @@ -30,21 +52,52 @@ # Sergey Stankevich # class python ( - $version = 'system', - $dev = false, - $virtualenv = false, - $gunicorn = false -) { + $version = $python::params::version, + $pip = $python::params::pip, + $dev = $python::params::dev, + $virtualenv = $python::params::virtualenv, + $gunicorn = $python::params::gunicorn, + $manage_gunicorn = $python::params::manage_gunicorn, + $provider = $python::params::provider, + $valid_versions = $python::params::valid_versions, + $python_pips = { }, + $python_virtualenvs = { }, + $python_pyvenvs = { }, +) inherits python::params{ + + # validate inputs + if $provider != undef { + validate_re($provider, ['^pip$'], 'Only "pip" is a valid provider besides the system default.') + } + + if $provider == 'pip' { + validate_re($version, ['^(2\.[4-7]\.\d|3\.\d\.\d)$','^system$']) + # this will only be checked if not pip, every other string would be rejected by provider check + } else { + validate_re($version, concat(['system', 'pypy'], $valid_versions)) + } + + validate_bool($pip) + validate_bool($dev) + validate_bool($virtualenv) + validate_bool($gunicorn) + validate_bool($manage_gunicorn) # Module compatibility check - $compatible = [ 'Debian', 'Ubuntu' ] - if ! ($::operatingsystem in $compatible) { + $compatible = [ 'Debian', 'RedHat'] + if ! ($::osfamily in $compatible) { fail("Module is not compatible with ${::operatingsystem}") } - Class['python::install'] -> Class['python::config'] + # Anchor pattern to contain dependencies + anchor { 'python::begin': } -> + class { 'python::install': } -> + class { 'python::config': } -> + anchor { 'python::end': } - include python::install - include python::config + # Allow hiera configuration of python resources + create_resources('python::pip', $python_pips) + create_resources('python::pyvenv', $python_pyvenvs) + create_resources('python::virtualenv', $python_virtualenvs) } diff --git a/manifests/install.pp b/manifests/install.pp index dec99b8a..fc97fae7 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -1,31 +1,74 @@ +# == Define: python::install +# +# Installs core python packages +# +# === Examples +# +# include python::install +# +# === Authors +# +# Sergey Stankevich +# Ashley Penney +# Fotis Gimian +# + class python::install { - $python = $python::version ? { + $python = $::python::version ? { 'system' => 'python', + 'pypy' => 'pypy', default => "python${python::version}", } - package { $python: ensure => present } + $pythondev = $::osfamily ? { + 'RedHat' => "${python}-devel", + 'Debian' => "${python}-dev" + } + + # pip version: use only for installation via os package manager! + if $::python::version =~ /^3/ { + $pip = 'python3-pip' + } else { + $pip = 'python-pip' + } $dev_ensure = $python::dev ? { true => present, default => absent, } - package { "${python}-dev": ensure => $dev_ensure } - - $venv_ensure = $python::virtualenv ? { + $pip_ensure = $python::pip ? { true => present, default => absent, } - package { 'python-virtualenv': ensure => $venv_ensure } - - $gunicorn_ensure = $python::gunicorn ? { + $venv_ensure = $python::virtualenv ? { true => present, default => absent, } - package { 'gunicorn': ensure => $gunicorn_ensure } + # Install latest from pip if pip is the provider + case $python::provider { + pip: { + package { 'virtualenv': ensure => latest, provider => pip } + package { 'pip': ensure => latest, provider => pip } + package { "python==${python::version}": ensure => latest, provider => pip } + } + default: { + package { 'python-virtualenv': ensure => $venv_ensure } + package { $pip: ensure => $pip_ensure } + package { $pythondev: ensure => $dev_ensure } + package { $python: ensure => present } + } + } + + if $python::manage_gunicorn { + $gunicorn_ensure = $python::gunicorn ? { + true => present, + default => absent, + } + package { 'gunicorn': ensure => $gunicorn_ensure } + } } diff --git a/manifests/params.pp b/manifests/params.pp new file mode 100644 index 00000000..107ebcfb --- /dev/null +++ b/manifests/params.pp @@ -0,0 +1,17 @@ + +# Class: python::params +# The python Module default configuration settings. +# +class python::params { + $version = 'system' + $pip = true + $dev = false + $virtualenv = false + $gunicorn = false + $manage_gunicorn = true + $provider = undef + $valid_versions = $::osfamily ? { + 'RedHat' => ['3'], + 'Debian' => ['2.7', '3', '3.3'], + } +} diff --git a/manifests/pip.pp b/manifests/pip.pp index 9762c103..18abc780 100644 --- a/manifests/pip.pp +++ b/manifests/pip.pp @@ -4,15 +4,47 @@ # # === Parameters # +# [*name] +# must be unique +# +# [*pkgname] +# name of the package. If pkgname is not specified, use name (title) instead. +# # [*ensure*] # present|absent. Default: present # # [*virtualenv*] # virtualenv to run pip in. # +# [*url*] +# URL to install from. Default: none +# +# [*owner*] +# The owner of the virtualenv being manipulated. Default: root +# # [*proxy*] # Proxy server to use for outbound connections. Default: none # +# [*editable*] +# Boolean. If true the package is installed as an editable resource. +# +# [*environment*] +# Additional environment variables required to install the packages. Default: none +# +# [*timeout*] +# The maximum time in seconds the "pip install" command should take. Default: 1800 +# +# [*install_args*] +# String. Any additional installation arguments that will be supplied +# when running pip install. +# +# [*uninstall_args*] +# String. Any additional arguments that will be supplied when running +# pip uninstall. +# +# [*log_dir*] +# String. Log directory. +# # === Examples # # python::pip { 'flask': @@ -23,11 +55,22 @@ # === Authors # # Sergey Stankevich +# Fotis Gimian # define python::pip ( - $virtualenv, - $ensure = present, - $proxy = false + $pkgname = $name, + $ensure = present, + $virtualenv = 'system', + $url = false, + $owner = 'root', + $proxy = false, + $egg = false, + $editable = false, + $environment = [], + $install_args = '', + $uninstall_args = '', + $timeout = 1800, + $log_dir = '/tmp', ) { # Parameter validation @@ -35,30 +78,163 @@ fail('python::pip: virtualenv parameter must not be empty') } + if $virtualenv == 'system' and $owner != 'root' { + fail('python::pip: root user must be used when virtualenv is system') + } + + $cwd = $virtualenv ? { + 'system' => '/', + default => $virtualenv, + } + + validate_absolute_path($cwd) + + $log = $virtualenv ? { + 'system' => $log_dir, + default => $virtualenv, + } + + $pip_env = $virtualenv ? { + 'system' => 'pip', + default => "${virtualenv}/bin/pip", + } + $proxy_flag = $proxy ? { false => '', default => "--proxy=${proxy}", } - $grep_regex = $name ? { - /==/ => "^${name}\$", - default => "^${name}==", + if $editable == true { + $install_editable = ' -e ' + } + else { + $install_editable = '' + } + + #TODO: Do more robust argument checking, but below is a start + if ($ensure == absent) and ($install_args != '') { + fail('python::pip cannot provide install_args with ensure => absent') } - case $ensure { - present: { - exec { "pip_install_${name}": - command => "${virtualenv}/bin/pip install ${proxy_flag} ${name}", - unless => "${virtualenv}/bin/pip freeze | grep -i -e ${grep_regex}", - } + if ($ensure == present) and ($uninstall_args != '') { + fail('python::pip cannot provide uninstall_args with ensure => present') + } + + # Check if searching by explicit version. + if $ensure =~ /^((19|20)[0-9][0-9]-(0[1-9]|1[1-2])-([0-2][1-9]|3[0-1])|[0-9]+\.[0-9]+(\.[0-9]+)?)$/ { + $grep_regex = "^${pkgname}==${ensure}\$" + } else { + $grep_regex = $pkgname ? { + /==/ => "^${pkgname}\$", + default => "^${pkgname}==", } + } - default: { - exec { "pip_uninstall_${name}": - command => "echo y | ${virtualenv}/bin/pip uninstall ${proxy_flag} ${name}", - onlyif => "${virtualenv}/bin/pip freeze | grep -i -e ${grep_regex}", - } + $egg_name = $egg ? { + false => $pkgname, + default => $egg + } + + $source = $url ? { + false => $pkgname, + /^(\/|[a-zA-Z]\:)/ => $url, + /^(git\+|hg\+|bzr\+|svn\+)(http|https|ssh|svn|sftp|ftp|lp)(:\/\/).+$/ => $url, + default => "${url}#egg=${egg_name}", + } + + # We need to jump through hoops to make sure we issue the correct pip command + # depending on wheel support and versions. + # + # Pip does not support wheels prior to version 1.4.0 + # Pip wheels require setuptools/distribute > 0.8 + # Python 2.6 and older does not support setuptools/distribute > 0.8 + # Pip >= 1.5 tries to use wheels by default, even if wheel package is not + # installed, in this case the --no-use-wheel flag needs to be passed + # Versions prior to 1.5 don't support the --no-use-wheel flag + # + # To check for this we test for wheel parameter using help and then using + # version, this makes sure we only use wheels if they are supported and + # installed + + # Explicit version out of VCS when PIP supported URL is provided + if $source =~ /^(git\+|hg\+|bzr\+|svn\+)(http|https|ssh|svn|sftp|ftp|lp)(:\/\/).+$/ { + if $ensure != present and $ensure != latest { + exec { "pip_install_${name}": + command => "${pip_env} wheel --help > /dev/null 2>&1 && { ${pip_env} wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { ${pip_env} --log ${log}/pip.log install ${install_args} \$wheel_support_flag ${proxy_flag} ${install_args} ${install_editable} ${source}@${ensure} || ${pip_env} --log ${log}/pip.log install ${install_args} ${proxy_flag} ${install_args} ${install_editable} ${source}@${ensure} ;}", + unless => "${pip_env} freeze | grep -i -e ${grep_regex}", + user => $owner, + cwd => $cwd, + environment => $environment, + path => ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'], + timeout => $timeout, + } + } + else { + exec { "pip_install_${name}": + command => "${pip_env} wheel --help > /dev/null 2>&1 && { ${pip_env} wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { ${pip_env} --log ${log}/pip.log install ${install_args} \$wheel_support_flag ${proxy_flag} ${install_args} ${install_editable} ${source} || ${pip_env} --log ${log}/pip.log install ${install_args} ${proxy_flag} ${install_args} ${install_editable} ${source} ;}", + unless => "${pip_env} freeze | grep -i -e ${grep_regex}", + user => $owner, + cwd => $cwd, + environment => $environment, + path => ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'], + timeout => $timeout, + } } } + else { + case $ensure { + /^((19|20)[0-9][0-9]-(0[1-9]|1[1-2])-([0-2][1-9]|3[0-1])|[0-9]+\.[0-9]+(\.[0-9]+)?)$/: { + # Version formats as per http://guide.python-distribute.org/specification.html#standard-versioning-schemes + # Explicit version. + exec { "pip_install_${name}": + command => "${pip_env} wheel --help > /dev/null 2>&1 && { ${pip_env} wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { ${pip_env} --log ${log}/pip.log install ${install_args} \$wheel_support_flag ${proxy_flag} ${install_args} ${install_editable} ${source}==${ensure} || ${pip_env} --log ${log}/pip.log install ${install_args} ${proxy_flag} ${install_args} ${install_editable} ${source}==${ensure} ;}", + unless => "${pip_env} freeze | grep -i -e ${grep_regex}", + user => $owner, + cwd => $cwd, + environment => $environment, + path => ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'], + timeout => $timeout, + } + } + + present: { + # Whatever version is available. + exec { "pip_install_${name}": + command => "${pip_env} wheel --help > /dev/null 2>&1 && { ${pip_env} wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { ${pip_env} --log ${log}/pip.log install \$wheel_support_flag ${proxy_flag} ${install_args} ${install_editable} ${source} || ${pip_env} --log ${log}/pip.log install ${proxy_flag} ${install_args} ${install_editable} ${source} ;}", + unless => "${pip_env} freeze | grep -i -e ${grep_regex}", + user => $owner, + cwd => $cwd, + environment => $environment, + path => ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'], + timeout => $timeout, + } + } + latest: { + # Latest version. + exec { "pip_install_${name}": + command => "${pip_env} wheel --help > /dev/null 2>&1 && { ${pip_env} wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { ${pip_env} --log ${log}/pip.log install --upgrade \$wheel_support_flag ${proxy_flag} ${install_args} ${install_editable} ${source} || ${pip_env} --log ${log}/pip.log install --upgrade ${proxy_flag} ${install_args} ${install_editable} ${source} ;}", + unless => "${pip_env} search ${proxy_flag} ${source} | grep -i INSTALLED | grep -i latest", + user => $owner, + cwd => $cwd, + environment => $environment, + path => ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'], + timeout => $timeout, + } + } + + default: { + # Anti-action, uninstall. + exec { "pip_uninstall_${name}": + command => "echo y | ${pip_env} uninstall ${uninstall_args} ${proxy_flag}", + onlyif => "${pip_env} freeze | grep -i -e ${grep_regex}", + user => $owner, + cwd => $cwd, + environment => $environment, + path => ['/usr/local/bin','/usr/bin','/bin', '/usr/sbin'], + timeout => $timeout, + } + } + } + } } diff --git a/manifests/pyvenv.pp b/manifests/pyvenv.pp new file mode 100644 index 00000000..b3ab6f47 --- /dev/null +++ b/manifests/pyvenv.pp @@ -0,0 +1,102 @@ +# == Define: python::pyvenv +# +# Create a Python3 virtualenv using pyvenv. +# +# === Parameters +# +# [*ensure*] +# present|absent. Default: present +# +# [*version*] +# Python version to use. Default: system default +# +# [*systempkgs*] +# Copy system site-packages into virtualenv. Default: don't +# +# [*venv_dir*] +# Directory to install virtualenv to. Default: $name +# +# [*owner*] +# The owner of the virtualenv being manipulated. Default: root +# +# [*group*] +# The group relating to the virtualenv being manipulated. Default: root +# +# [*mode*] +# Optionally specify directory mode. Default: 0755 +# +# [*path*] +# Specifies the PATH variable. Default: [ '/bin', '/usr/bin', '/usr/sbin' ] + +# [*environment*] +# Optionally specify environment variables for pyvenv +# +# === Examples +# +# python::venv { '/var/www/project1': +# ensure => present, +# version => 'system', +# systempkgs => true, +# } +# +# === Authors +# +# Sergey Stankevich +# Ashley Penney +# Marc Fournier +# Fotis Gimian +# Seth Cleveland +# +define python::pyvenv ( + $ensure = present, + $version = 'system', + $systempkgs = false, + $venv_dir = $name, + $owner = 'root', + $group = 'root', + $mode = '0755', + $path = [ '/bin', '/usr/bin', '/usr/sbin' ], + $environment = [], +) { + + if $ensure == 'present' { + + $virtualenv_cmd = $version ? { + 'system' => 'pyvenv', + default => "pyvenv-${version}", + } + + if ( $systempkgs == true ) { + $system_pkgs_flag = '--system-site-packages' + } else { + $system_pkgs_flag = '' + } + + file { $venv_dir: + ensure => directory, + owner => $owner, + group => $group, + mode => $mode + } + + exec { "python_virtualenv_${venv_dir}": + command => "${virtualenv_cmd} ${system_pkgs_flag} ${venv_dir}", + user => $owner, + creates => "${venv_dir}/bin/activate", + path => $path, + cwd => '/tmp', + environment => $environment, + unless => "grep '^[\\t ]*VIRTUAL_ENV=[\\\\'\\\"]*${venv_dir}[\\\"\\\\'][\\t ]*$' ${venv_dir}/bin/activate", #Unless activate exists and VIRTUAL_ENV is correct we re-create the virtualenv + require => File[$venv_dir], + } + } elsif $ensure == 'absent' { + file { $venv_dir: + ensure => absent, + force => true, + recurse => true, + purge => true, + } + } else { + fail( "Illegal ensure value: ${ensure}. Expected (present or absent)") + } +} diff --git a/manifests/requirements.pp b/manifests/requirements.pp index b6c9b346..b18c642b 100644 --- a/manifests/requirements.pp +++ b/manifests/requirements.pp @@ -4,12 +4,49 @@ # # === Parameters # +# [*requirements*] +# Path to the requirements file. Defaults to the resource name +# # [*virtualenv*] # virtualenv to run pip in. Default: system-wide # +# [*owner*] +# The owner of the virtualenv being manipulated. Default: root +# +# [*group*] +# The group relating to the virtualenv being manipulated. Default: root +# # [*proxy*] # Proxy server to use for outbound connections. Default: none # +# [*src*] +# Pip --src parameter; if the requirements file contains --editable resources, +# this parameter specifies where they will be installed. See the pip +# documentation for more. Default: none (i.e. use the pip default). +# +# [*environment*] +# Additional environment variables required to install the packages. Default: none +# +# [*forceupdate*] +# Run a pip install requirements even if we don't receive an event from the +# requirements file - Useful for when the requirements file is written as part of a +# resource other than file (E.g vcsrepo) +# +# [*cwd*] +# The directory from which to run the "pip install" command. Default: undef +# +# [*extra_pip_args*] +# Extra arguments to pass to pip after the requirements file +# +# [*fix_requirements_owner*] +# Change owner and group of requirements file. Default: true +# +# [*log_dir*] +# String. Log directory. +# +# [*timeout*] +# The maximum time in seconds the "pip install" command should take. Default: 1800 +# # === Examples # # python::requirements { '/var/www/project1/requirements.txt': @@ -20,16 +57,44 @@ # === Authors # # Sergey Stankevich +# Ashley Penney +# Fotis Gimian # define python::requirements ( - $virtualenv = 'system', - $proxy = false + $requirements = $name, + $virtualenv = 'system', + $owner = 'root', + $group = 'root', + $proxy = false, + $src = false, + $environment = [], + $forceupdate = false, + $cwd = undef, + $extra_pip_args = '', + $fix_requirements_owner = true, + $log_dir = '/tmp', + $timeout = 1800, ) { - $requirements = $name + if $virtualenv == 'system' and ($owner != 'root' or $group != 'root') { + fail('python::pip: root user must be used when virtualenv is system') + } + + if $fix_requirements_owner { + $owner_real = $owner + $group_real = $group + } else { + $owner_real = undef + $group_real = undef + } + + $log = $virtualenv ? { + 'system' => $log_dir, + default => $virtualenv, + } $pip_env = $virtualenv ? { - 'system' => '`which pip`', + 'system' => 'pip', default => "${virtualenv}/bin/pip", } @@ -38,31 +103,34 @@ default => "--proxy=${proxy}", } - $req_dir = inline_template('<%= requirements.match(%r!(.+)/.+!)[1] %>') - $req_crc = "${requirements}.sha1" - - file { $requirements: - ensure => present, - mode => '0644', - owner => 'root', - group => 'root', - replace => false, - content => '# Puppet will install and/or update pip packages listed here', + $src_flag = $src ? { + false => '', + default => "--src=${src}", } - # SHA1 checksum to detect changes - exec { "python_requirements_check_${name}": - command => "sha1sum ${requirements} > ${req_crc}", - unless => "sha1sum -c ${req_crc}", - require => File[$requirements], + # This will ensure multiple python::virtualenv definitions can share the + # the same requirements file. + if !defined(File[$requirements]) { + file { $requirements: + ensure => present, + mode => '0644', + owner => $owner_real, + group => $group_real, + audit => content, + replace => false, + content => '# Puppet will install and/or update pip packages listed here', + } } - exec { "python_requirements_update_${name}": - command => "${pip_env} install ${proxy_flag} -Ur ${requirements}", - cwd => $virtualenv, - refreshonly => true, - timeout => 1800, - subscribe => Exec["python_requirements_check_${name}"], + exec { "python_requirements${name}": + provider => shell, + command => "${pip_env} --log ${log}/pip.log install ${proxy_flag} ${src_flag} -r ${requirements} ${extra_pip_args}", + refreshonly => !$forceupdate, + timeout => $timeout, + cwd => $cwd, + user => $owner, + subscribe => File[$requirements], + environment => $environment, } } diff --git a/manifests/virtualenv.pp b/manifests/virtualenv.pp index f22f4e14..62be1ed5 100644 --- a/manifests/virtualenv.pp +++ b/manifests/virtualenv.pp @@ -13,9 +13,45 @@ # [*requirements*] # Path to pip requirements.txt file. Default: none # +# [*systempkgs*] +# Copy system site-packages into virtualenv. Default: don't +# If virtualenv version < 1.7 this flag has no effect since +# [*venv_dir*] +# Directory to install virtualenv to. Default: $name +# +# [*distribute*] +# Include distribute in the virtualenv. Default: true +# +# [*index*] +# Base URL of Python package index. Default: none (http://pypi.python.org/simple/) +# +# [*owner*] +# The owner of the virtualenv being manipulated. Default: root +# +# [*group*] +# The group relating to the virtualenv being manipulated. Default: root +# +# [*mode*] +# Optionally specify directory mode. Default: 0755 +# # [*proxy*] # Proxy server to use for outbound connections. Default: none # +# [*environment*] +# Additional environment variables required to install the packages. Default: none +# +# [*path*] +# Specifies the PATH variable. Default: [ '/bin', '/usr/bin', '/usr/sbin' ] +# +# [*cwd*] +# The directory from which to run the "pip install" command. Default: undef +# +# [*timeout*] +# The maximum time in seconds the "pip install" command should take. Default: 1800 +# +# [*extra_pip_args*] +# Extra arguments to pass to pip after requirements file. Default: blank +# # === Examples # # python::virtualenv { '/var/www/project1': @@ -23,28 +59,49 @@ # version => 'system', # requirements => '/var/www/project1/requirements.txt', # proxy => 'http://proxy.domain.com:3128', +# systempkgs => true, +# index => 'http://www.example.com/simple/' # } # # === Authors # # Sergey Stankevich +# Ashley Penney +# Marc Fournier +# Fotis Gimian # define python::virtualenv ( - $ensure = present, - $version = 'system', - $requirements = false, - $proxy = false + $ensure = present, + $version = 'system', + $requirements = false, + $systempkgs = false, + $venv_dir = $name, + $distribute = true, + $index = false, + $owner = 'root', + $group = 'root', + $mode = '0755', + $proxy = false, + $environment = [], + $path = [ '/bin', '/usr/bin', '/usr/sbin' ], + $cwd = undef, + $timeout = 1800, + $extra_pip_args = '' ) { - $venv_dir = $name - if $ensure == 'present' { $python = $version ? { 'system' => 'python', + 'pypy' => 'pypy', default => "python${version}", } + $virtualenv = $version ? { + 'system' => 'virtualenv', + default => "virtualenv-${version}", + } + $proxy_flag = $proxy ? { false => '', default => "--proxy=${proxy}", @@ -55,24 +112,80 @@ default => "&& export http_proxy=${proxy}", } + # Virtualenv versions prior to 1.7 do not support the + # --system-site-packages flag, default off for prior versions + # Prior to version 1.7 the default was equal to --system-site-packages + # and the flag --no-site-packages had to be passed to do the opposite + if (( versioncmp($::virtualenv_version,'1.7') > 0 ) and ( $systempkgs == true )) { + $system_pkgs_flag = '--system-site-packages' + } elsif (( versioncmp($::virtualenv_version,'1.7') < 0 ) and ( $systempkgs == false )) { + $system_pkgs_flag = '--no-site-packages' + } else { + $system_pkgs_flag = $systempkgs ? { + true => '--system-site-packages', + false => '--no-site-packages', + default => fail('Invalid value for systempkgs. Boolean value is expected') + } + } + + $distribute_pkg = $distribute ? { + true => 'distribute', + default => 'setuptools', + } + $pypi_index = $index ? { + false => '', + default => "-i ${index}", + } + + # Python 2.6 and older does not support setuptools/distribute > 0.8 which + # is required for pip wheel support, pip therefor requires --no-use-wheel flag + # if the # pip version is more recent than 1.4.1 but using an old python or + # setuputils/distribute version + # To check for this we test for wheel parameter using help and then using + # version, this makes sure we only use wheels if they are supported + + file { $venv_dir: + ensure => directory, + owner => $owner, + group => $group, + mode => $mode + } + + + exec { "python_virtualenv_${venv_dir}": - command => "mkdir -p ${venv_dir} \ - ${proxy_command} \ - && virtualenv -p `which ${python}` ${venv_dir} \ - && ${venv_dir}/bin/pip install ${proxy_flag} --upgrade distribute pip", - creates => $venv_dir, + command => "true ${proxy_command} && ${virtualenv} ${system_pkgs_flag} -p ${python} ${venv_dir} && ${venv_dir}/bin/pip wheel --help > /dev/null 2>&1 && { ${venv_dir}/bin/pip wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { ${venv_dir}/bin/pip --log ${venv_dir}/pip.log install ${pypi_index} ${proxy_flag} \$wheel_support_flag --upgrade pip ${distribute_pkg} || ${venv_dir}/bin/pip --log ${venv_dir}/pip.log install ${pypi_index} ${proxy_flag} --upgrade pip ${distribute_pkg} ;}", + user => $owner, + creates => "${venv_dir}/bin/activate", + path => $path, + cwd => '/tmp', + environment => $environment, + unless => "grep '^[\\t ]*VIRTUAL_ENV=[\\\\'\\\"]*${venv_dir}[\\\"\\\\'][\\t ]*$' ${venv_dir}/bin/activate", #Unless activate exists and VIRTUAL_ENV is correct we re-create the virtualenv + require => File[$venv_dir], } if $requirements { - Exec["python_virtualenv_${venv_dir}"] - -> Python::Requirements[$requirements] + exec { "python_requirements_initial_install_${requirements}_${venv_dir}": + command => "${venv_dir}/bin/pip wheel --help > /dev/null 2>&1 && { ${venv_dir}/bin/pip wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; ${venv_dir}/bin/pip --log ${venv_dir}/pip.log install ${pypi_index} ${proxy_flag} \$wheel_support_flag -r ${requirements} ${extra_pip_args}", + refreshonly => true, + timeout => $timeout, + user => $owner, + subscribe => Exec["python_virtualenv_${venv_dir}"], + environment => $environment, + cwd => $cwd + } - python::requirements { $requirements: - virtualenv => $venv_dir, - proxy => $proxy, + python::requirements { "${requirements}_${venv_dir}": + requirements => $requirements, + virtualenv => $venv_dir, + proxy => $proxy, + owner => $owner, + group => $group, + cwd => $cwd, + require => Exec["python_virtualenv_${venv_dir}"], + extra_pip_args => $extra_pip_args, } } - } elsif $ensure == 'absent' { file { $venv_dir: @@ -81,7 +194,5 @@ recurse => true, purge => true, } - } - } diff --git a/metadata.json b/metadata.json new file mode 100644 index 00000000..c8296d23 --- /dev/null +++ b/metadata.json @@ -0,0 +1,49 @@ +{ + "name": "stankevich-python", + "version": "1.8.0", + "source": "git://github.com/stankevich/puppet-python.git", + "author": "stankevich", + "license": "Apache Version 2.0", + "summary": "Python Module", + "description": "Puppet module for Python", + "project_page": "https://github.com/stankevich/puppet-python", + "issues_url": "https://github.com/stankevich/puppet-python/issues", + "tags": ["python", "pip", "virtualenv", "gunicorn"], + "operatingsystem_support": [ + { + "operatingsystem": "CentOS", + "operatingsystemrelease": [ + "5", + "6", + "7" + ] + }, + { + "operatingsystem": "Debian", + "operatingsystemrelease": [ + "6", + "7" + ] + }, + { + "operatingsystem": "Ubuntu", + "operatingsystemrelease": [ + "10.04", + "12.04", + "14.04" + ] + } + ], + "requirements": [ + { + "name": "puppet", + "version_requirement": "3.x" + } + ], + "dependencies": [ + { + "name": "puppetlabs/stdlib", + "version_requirement": ">= 4.0.0" + } + ] +} diff --git a/spec/acceptance/class_spec.rb b/spec/acceptance/class_spec.rb new file mode 100644 index 00000000..0b65659c --- /dev/null +++ b/spec/acceptance/class_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper_acceptance' + +describe 'python class' do + + context 'default parameters' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'python': } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + end +end diff --git a/spec/acceptance/facts_test_spec.rb b/spec/acceptance/facts_test_spec.rb new file mode 100644 index 00000000..bdfd6009 --- /dev/null +++ b/spec/acceptance/facts_test_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper_acceptance' + +describe 'python class' do + + context 'facts' do + install_python = <<-EOS + class { 'python' : + version => 'system', + pip => true, + virtualenv => true, + } + EOS + + fact_notices = <<-EOS + notify{"pip_version: ${::pip_version}":} + notify{"system_python_version: ${::system_python_version}":} + notify{"python_version: ${::python_version}":} + notify{"virtualenv_version: ${::virtualenv_version}":} + EOS + + it 'should output python facts when not installed' do + apply_manifest(fact_notices, :catch_failures => true) do |r| + expect(r.stdout).to match(/python_version: \S+/) + expect(r.stdout).to match(/pip_version: \S+/) + expect(r.stdout).to match(/virtualenv_version: \S+/) + expect(r.stdout).to match(/system_python_version: \S+/) + end + end + + it 'sets up python' do + apply_manifest(install_python, :catch_failures => true) + end + + it 'should output python facts when installed' do + apply_manifest(fact_notices, :catch_failures => true) do |r| + expect(r.stdout).to match(/python_version: \S+/) + expect(r.stdout).to match(/pip_version: \S+/) + expect(r.stdout).to match(/virtualenv_version: \S+/) + expect(r.stdout).to match(/system_python_version: \S+/) + end + end + end +end diff --git a/spec/acceptance/nodesets/centos-59-x64.yml b/spec/acceptance/nodesets/centos-59-x64.yml new file mode 100644 index 00000000..2ad90b86 --- /dev/null +++ b/spec/acceptance/nodesets/centos-59-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-59-x64: + roles: + - master + platform: el-5-x86_64 + box : centos-59-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: git diff --git a/spec/acceptance/nodesets/centos-64-x64-pe.yml b/spec/acceptance/nodesets/centos-64-x64-pe.yml new file mode 100644 index 00000000..7d9242f1 --- /dev/null +++ b/spec/acceptance/nodesets/centos-64-x64-pe.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-64-x64: + roles: + - master + - database + - dashboard + platform: el-6-x86_64 + box : centos-64-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: pe diff --git a/spec/acceptance/nodesets/centos-65-x64.yml b/spec/acceptance/nodesets/centos-65-x64.yml new file mode 100644 index 00000000..4e2cb809 --- /dev/null +++ b/spec/acceptance/nodesets/centos-65-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-65-x64: + roles: + - master + platform: el-6-x86_64 + box : centos-65-x64-vbox436-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-65-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/spec/acceptance/nodesets/debian-70rc1-x64.yml b/spec/acceptance/nodesets/debian-70rc1-x64.yml new file mode 100644 index 00000000..4b55677f --- /dev/null +++ b/spec/acceptance/nodesets/debian-70rc1-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-70rc1-x64: + roles: + - master + platform: debian-70rc1-x64 + box : debian-70rc1-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/spec/acceptance/nodesets/debian-73-x64.yml b/spec/acceptance/nodesets/debian-73-x64.yml new file mode 100644 index 00000000..d7143455 --- /dev/null +++ b/spec/acceptance/nodesets/debian-73-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + debian-73-x64: + roles: + - master + platform: debian-7-amd64 + box : debian-73-x64-virtualbox-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-73-x64-virtualbox-nocm.box + hypervisor : vagrant +CONFIG: + log_level: debug + type: git \ No newline at end of file diff --git a/spec/acceptance/nodesets/default.yml b/spec/acceptance/nodesets/default.yml new file mode 100644 index 00000000..45af9893 --- /dev/null +++ b/spec/acceptance/nodesets/default.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-server-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss + vagrant_ssh_port_random: true diff --git a/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml new file mode 100644 index 00000000..5ca1514e --- /dev/null +++ b/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-10044-x64: + roles: + - master + platform: ubuntu-10.04-amd64 + box : ubuntu-server-10044-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml new file mode 100644 index 00000000..d065b304 --- /dev/null +++ b/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-server-12042-x64: + roles: + - master + platform: ubuntu-12.04-amd64 + box : ubuntu-server-12042-x64-vbox4210-nocm + box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box + hypervisor : vagrant +CONFIG: + type: foss diff --git a/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml b/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml new file mode 100644 index 00000000..cba1cd04 --- /dev/null +++ b/spec/acceptance/nodesets/ubuntu-server-1404-x64.yml @@ -0,0 +1,11 @@ +HOSTS: + ubuntu-server-1404-x64: + roles: + - master + platform: ubuntu-14.04-amd64 + box : puppetlabs/ubuntu-14.04-64-nocm + box_url : https://vagrantcloud.com/puppetlabs/ubuntu-14.04-64-nocm + hypervisor : vagrant +CONFIG: + log_level : debug + type: git diff --git a/spec/acceptance/virtualenv_spec.rb b/spec/acceptance/virtualenv_spec.rb new file mode 100644 index 00000000..efc844fe --- /dev/null +++ b/spec/acceptance/virtualenv_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper_acceptance' + +describe 'python class' do + + context 'default parameters' do + # Using puppet_apply as a helper + it 'should work with no errors' do + pp = <<-EOS + class { 'python' : + version => 'system', + pip => true, + virtualenv => true, + } + -> + python::virtualenv { 'venv' : + ensure => present, + systempkgs => false, + venv_dir => '/opt/venv', + owner => 'root', + group => 'root', + } + -> + python::pip { 'rpyc' : + ensure => '3.2.3', + virtualenv => '/opt/venv', + } + EOS + + # Run it twice and test for idempotency + apply_manifest(pp, :catch_failures => true) + apply_manifest(pp, :catch_changes => true) + end + end +end diff --git a/spec/classes/python_spec.rb b/spec/classes/python_spec.rb new file mode 100644 index 00000000..cc7ac9bd --- /dev/null +++ b/spec/classes/python_spec.rb @@ -0,0 +1,184 @@ +require 'spec_helper' + +describe 'python', :type => :class do + context "on Debian OS" do + let :facts do + { + :id => 'root', + :kernel => 'Linux', + :lsbdistcodename => 'squeeze', + :osfamily => 'Debian', + :operatingsystem => 'Debian', + :operatingsystemrelease => '6', + :path => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + :concat_basedir => '/dne', + } + end + + it { is_expected.to contain_class("python::install") } + # Base debian packages. + it { is_expected.to contain_package("python") } + it { is_expected.to contain_package("python-dev") } + it { is_expected.to contain_package("python-pip") } + # Basic python packages (from pip) + it { is_expected.to contain_package("python-virtualenv")} + + describe "with python::dev" do + context "true" do + let (:params) {{ :dev => true }} + it { is_expected.to contain_package("python-dev").with_ensure('present') } + end + context "empty/default" do + it { is_expected.to contain_package("python-dev").with_ensure('absent') } + end + end + + describe "with manage_gunicorn" do + context "true" do + let (:params) {{ :manage_gunicorn => true }} + it { is_expected.to contain_package("gunicorn") } + end + context "empty args" do + #let (:params) {{ :manage_gunicorn => '' }} + it { is_expected.to contain_package("gunicorn") } + end + context "false" do + let (:params) {{ :manage_gunicorn => false }} + it {is_expected.not_to contain_package("gunicorn")} + end + end + + describe "with python::provider" do + context "pip" do + let (:params) {{ :provider => 'pip' }} + it { is_expected.to contain_package("virtualenv").with( + 'provider' => 'pip' + )} + it { is_expected.to contain_package("pip").with( + 'provider' => 'pip' + )} + end + + # python::provider + context "default" do + let (:params) {{ :provider => '' }} + it { is_expected.to contain_package("python-virtualenv")} + it { is_expected.to contain_package("python-pip")} + + describe "with python::virtualenv" do + context "true" do + let (:params) {{ :provider => '', :virtualenv => true }} + it { is_expected.to contain_package("python-virtualenv").with_ensure('present') } + end + end + + describe "without python::virtualenv" do + context "default/empty" do + let (:params) {{ :provider => '' }} + it { is_expected.to contain_package("python-virtualenv").with_ensure('absent') } + end + end + end + end + + describe "with python::dev" do + context "true" do + let (:params) {{ :dev => true }} + it { is_expected.to contain_package("python-dev").with_ensure('present') } + end + context "default/empty" do + it { is_expected.to contain_package("python-dev").with_ensure('absent') } + end + end + end + + context "on a Redhat 5 OS" do + let :facts do + { + :id => 'root', + :kernel => 'Linux', + :osfamily => 'RedHat', + :operatingsystem => 'RedHat', + :operatingsystemrelease => '5', + :concat_basedir => '/dne', + :path => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + } + end + it { is_expected.to contain_class("python::install") } + # Base debian packages. + it { is_expected.to contain_package("python") } + it { is_expected.to contain_package("python-devel") } + it { is_expected.to contain_package("python-pip") } + # Basic python packages (from pip) + it { is_expected.to contain_package("python-virtualenv")} + + describe "with python::dev" do + context "true" do + let (:params) {{ :dev => true }} + it { is_expected.to contain_package("python-devel").with_ensure('present') } + end + context "empty/default" do + it { is_expected.to contain_package("python-devel").with_ensure('absent') } + end + end + + describe "with manage_gunicorn" do + context "true" do + let (:params) {{ :manage_gunicorn => true }} + it { is_expected.to contain_package("gunicorn") } + end + context "empty args" do + #let (:params) {{ :manage_gunicorn => '' }} + it { is_expected.to contain_package("gunicorn") } + end + context "false" do + let (:params) {{ :manage_gunicorn => false }} + it {is_expected.not_to contain_package("gunicorn")} + end + end + + describe "with python::provider" do + context "pip" do + let (:params) {{ :provider => 'pip' }} + + it { is_expected.to contain_package("virtualenv").with( + 'provider' => 'pip' + )} + it { is_expected.to contain_package("pip").with( + 'provider' => 'pip' + )} + end + + # python::provider + context "default" do + let (:params) {{ :provider => '' }} + it { is_expected.to contain_package("python-virtualenv")} + it { is_expected.to contain_package("python-pip")} + + describe "with python::virtualenv" do + context "true" do + let (:params) {{ :provider => '', :virtualenv => true }} + it { is_expected.to contain_package("python-virtualenv").with_ensure('present') } + end + end + + describe "with python::virtualenv" do + context "default/empty" do + let (:params) {{ :provider => '' }} + it { is_expected.to contain_package("python-virtualenv").with_ensure('absent') } + end + end + end + end + + describe "with python::dev" do + context "true" do + let (:params) {{ :dev => true }} + it { is_expected.to contain_package("python-devel").with_ensure('present') } + end + context "default/empty" do + it { is_expected.to contain_package("python-devel").with_ensure('absent') } + end + end + end +end diff --git a/spec/defines/pip_spec.rb b/spec/defines/pip_spec.rb new file mode 100644 index 00000000..b4170104 --- /dev/null +++ b/spec/defines/pip_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'python::pip', :type => :define do + let (:title) { 'rpyc' } + context "on Debian OS" do + let :facts do + { + :id => 'root', + :kernel => 'Linux', + :lsbdistcodename => 'squeeze', + :osfamily => 'Debian', + :operatingsystem => 'Debian', + :operatingsystemrelease => '6', + :path => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + :concat_basedir => '/dne', + } + end + + describe "virtualenv as" do + context "fails with non qualified path" do + let (:params) {{ :virtualenv => "venv" }} + it { is_expected.to raise_error(/"venv" is not an absolute path./) } + end + context "suceeds with qualified path" do + let (:params) {{ :virtualenv => "/opt/venv" }} + it { is_expected.to contain_exec("pip_install_rpyc").with_cwd('/opt/venv') } + end + context "defaults to system" do + let (:params) {{ }} + it { is_expected.to contain_exec("pip_install_rpyc").with_cwd('/') } + end + end + + describe "proxy as" do + context "defaults to empty" do + let (:params) {{ }} + it { is_expected.to contain_exec("pip_install_rpyc").without_command(/--proxy/) } + end + context "does not add proxy to search command if set to latest and proxy is unset" do + let (:params) {{ :ensure => 'latest' }} + it { is_expected.to contain_exec("pip_install_rpyc").without_command(/--proxy/) } + it { is_expected.to contain_exec("pip_install_rpyc").without_unless(/--proxy/) } + end + context "adds proxy to install command if proxy set" do + let (:params) {{ :proxy => "http://my.proxy:3128" }} + it { is_expected.to contain_exec("pip_install_rpyc").with_command("pip wheel --help > /dev/null 2>&1 && { pip wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { pip --log /tmp/pip.log install $wheel_support_flag --proxy=http://my.proxy:3128 rpyc || pip --log /tmp/pip.log install --proxy=http://my.proxy:3128 rpyc ;}") } + end + context "adds proxy to search command if set to latest" do + let (:params) {{ :proxy => "http://my.proxy:3128", :ensure => 'latest' }} + it { is_expected.to contain_exec("pip_install_rpyc").with_command("pip wheel --help > /dev/null 2>&1 && { pip wheel --version > /dev/null 2>&1 || wheel_support_flag='--no-use-wheel'; } ; { pip --log /tmp/pip.log install --upgrade $wheel_support_flag --proxy=http://my.proxy:3128 rpyc || pip --log /tmp/pip.log install --upgrade --proxy=http://my.proxy:3128 rpyc ;}") } + it { is_expected.to contain_exec("pip_install_rpyc").with_unless('pip search --proxy=http://my.proxy:3128 rpyc | grep -i INSTALLED | grep -i latest') } + end + end + + end +end \ No newline at end of file diff --git a/spec/defines/pyvenv_spec.rb b/spec/defines/pyvenv_spec.rb new file mode 100644 index 00000000..06562eef --- /dev/null +++ b/spec/defines/pyvenv_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'python::pyvenv', :type => :define do + let (:title) { '/opt/env' } + + it { + should contain_file( '/opt/env') + should contain_exec( "python_virtualenv_/opt/env").with_command("pyvenv /opt/env") + } + + describe 'when ensure' do + context "is absent" do + let (:params) {{ + :ensure => 'absent' + }} + it { + should contain_file( '/opt/env').with_ensure('absent').with_purge( true) + } + end + end +end diff --git a/spec/defines/requirements_spec.rb b/spec/defines/requirements_spec.rb new file mode 100644 index 00000000..e1cdc28b --- /dev/null +++ b/spec/defines/requirements_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe 'python::requirements', :type => :define do + let (:title) { '/requirements.txt' } + context "on Debian OS" do + let :facts do + { + :id => 'root', + :kernel => 'Linux', + :lsbdistcodename => 'squeeze', + :osfamily => 'Debian', + :operatingsystem => 'Debian', + :operatingsystemrelease => '6', + :path => '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', + :concat_basedir => '/dne', + } + end + + describe "requirements as" do + context "/requirements.txt" do + let (:params) {{ :requirements => "/requirements.txt" }} + it { is_expected.to contain_file("/requirements.txt").with_mode('0644') } + end + + describe "with owner" do + context "bob:bob" do + let (:params) {{ + :owner => 'bob', + :group => 'bob' + }} + it do + expect { + should compile + }.to raise_error(/root user must be used when virtualenv is system/) + end + end + end + + describe "with owner" do + context "default" do + it { is_expected.to contain_file("/requirements.txt").with_owner('root').with_group('root') } + end + end + end + end +end diff --git a/spec/spec.opts b/spec/spec.opts new file mode 100644 index 00000000..91cd6427 --- /dev/null +++ b/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..2c6f5664 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1 @@ +require 'puppetlabs_spec_helper/module_spec_helper' diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb new file mode 100644 index 00000000..c96a75c4 --- /dev/null +++ b/spec/spec_helper_acceptance.rb @@ -0,0 +1,32 @@ +require 'beaker-rspec' + +UNSUPPORTED_PLATFORMS = [ 'windows' ] + +unless ENV['RS_PROVISION'] == 'no' or ENV['BEAKER_provision'] == 'no' + hosts.each do |host| + if host.is_pe? + install_pe + else + install_puppet + end + end +end + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + # Install module and dependencies + hosts.each do |host| + shell("rm -rf /etc/puppet/modules/python/") + copy_module_to(host, :source => proj_root, :module_name => 'python') + shell("/bin/touch #{default['puppetpath']}/hiera.yaml") + on host, puppet('module install puppetlabs-stdlib'), { :acceptable_exit_codes => [0,1] } + end + end +end diff --git a/spec/unit/facter/pip_version_spec.rb b/spec/unit/facter/pip_version_spec.rb new file mode 100644 index 00000000..2c75cbf0 --- /dev/null +++ b/spec/unit/facter/pip_version_spec.rb @@ -0,0 +1,32 @@ +require "spec_helper" + +describe Facter::Util::Fact do + before { + Facter.clear + } + + let(:pip_version_output) { <<-EOS +pip 6.0.6 from /opt/boxen/homebrew/Cellar/python/2.7.9/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/pip-6.0.6-py2.7.egg (python 2.7) +EOS + } + + describe "pip_version" do + context 'returns pip version when pip present' do + it do + Facter::Util::Resolution.stubs(:exec) + Facter::Util::Resolution.expects(:which).with("pip").returns(true) + Facter::Util::Resolution.expects(:exec).with("pip --version 2>&1").returns(pip_version_output) + Facter.value(:pip_version).should == "6.0.6" + end + end + + context 'returns nil when pip not present' do + it do + Facter::Util::Resolution.stubs(:exec) + Facter::Util::Resolution.expects(:which).with("pip").returns(false) + Facter.value(:pip_version).should == nil + end + end + + end +end diff --git a/spec/unit/facter/python_version_spec.rb b/spec/unit/facter/python_version_spec.rb new file mode 100644 index 00000000..0610a278 --- /dev/null +++ b/spec/unit/facter/python_version_spec.rb @@ -0,0 +1,32 @@ +require "spec_helper" + +describe Facter::Util::Fact do + before { + Facter.clear + } + + let(:python_version_output) { <<-EOS +Python 2.7.9 +EOS + } + + describe "python_version" do + context 'returns python version when python present' do + it do + Facter::Util::Resolution.stubs(:exec) + Facter::Util::Resolution.expects(:which).with("python").returns(true) + Facter::Util::Resolution.expects(:exec).with("python -V 2>&1").returns(python_version_output) + Facter.value(:python_version).should == "2.7.9" + end + end + + context 'returns nil when python not present' do + it do + Facter::Util::Resolution.stubs(:exec) + Facter::Util::Resolution.expects(:which).with("python").returns(false) + Facter.value(:python_version).should == nil + end + end + + end +end diff --git a/spec/unit/facter/virtualenv_version_spec.rb b/spec/unit/facter/virtualenv_version_spec.rb new file mode 100644 index 00000000..9f1cc921 --- /dev/null +++ b/spec/unit/facter/virtualenv_version_spec.rb @@ -0,0 +1,32 @@ +require "spec_helper" + +describe Facter::Util::Fact do + before { + Facter.clear + } + + let(:virtualenv_version_output) { <<-EOS +12.0.7 +EOS + } + + describe "virtualenv_version" do + context 'returns virtualenv version when virtualenv present' do + it do + Facter::Util::Resolution.stubs(:exec) + Facter::Util::Resolution.expects(:which).with("virtualenv").returns(true) + Facter::Util::Resolution.expects(:exec).with("virtualenv --version 2>&1").returns(virtualenv_version_output) + Facter.value(:virtualenv_version).should == "12.0.7" + end + end + + context 'returns nil when virtualenv not present' do + it do + Facter::Util::Resolution.stubs(:exec) + Facter::Util::Resolution.expects(:which).with("virtualenv").returns(false) + Facter.value(:virtualenv_version).should == nil + end + end + + end +end diff --git a/templates/gunicorn.erb b/templates/gunicorn.erb index 2cf3eedd..8e435bb8 100644 --- a/templates/gunicorn.erb +++ b/templates/gunicorn.erb @@ -1,35 +1,49 @@ CONFIG = { -<% if mode == 'django' -%> +<% if @mode == 'django' -%> 'mode': 'django', <% else -%> 'mode': 'wsgi', <% end -%> -<% if virtualenv -%> +<% if @virtualenv -%> 'environment': { -<% if environment -%> - 'ENVIRONMENT': '<%= environment %>', +<% if @osenv -%><% @osenv.sort.each do |key, value| -%> + '<%= key %>': '<%= value %>', +<% end -%><% end -%> +<% if @environment -%> + 'ENVIRONMENT': '<%= @environment %>', <% end -%> - 'PYTHONPATH': '<%= virtualenv %>' + 'PYTHONPATH': '<%= @virtualenv %>' }, <% end -%> - 'working_dir': '<%= dir %>', - 'user': 'www-data', - 'group': 'www-data', -<% if virtualenv -%> - 'python': '<%= virtualenv %>/bin/python', + 'working_dir': '<%= @dir %>', + 'user': '<%= @owner %>', + 'group': '<%= @group %>', +<% if @virtualenv -%> + 'python': '<%= @virtualenv %>/bin/python', <% else -%> 'python': '/usr/bin/python', <% end -%> 'args': ( -<% if !virtualenv and !bind -%> - '--bind=unix:/tmp/gunicorn-<%= name %>.socket', -<% elsif virtualenv and !bind -%> - '--bind=unix:<%= virtualenv %>/<%= name %>.socket', +<% if !@virtualenv and !@bind -%> + '--bind=unix:/tmp/gunicorn-<%= @name %>.socket', +<% elsif @virtualenv and !@bind -%> + '--bind=unix:<%= @virtualenv %>/<%= @name %>.socket', <% else -%> - '--bind=<%= bind %>', + '--bind=<%= @bind %>', <% end -%> '--workers=<%= @processorcount.to_i*2 %>', - '--timeout=30', - 'app:app', + '--timeout=<%= @timeout %>', +<% if @access_log_format -%> + '--access-logformat=<%= @access_log_format %>', +<% end -%> +<% if @accesslog -%> + '--access-logfile=<%= @accesslog %>', +<% end -%> +<% if @errorlog -%> + '--error-logfile=<%= @errorlog %>', +<% end -%> +<% if @mode != 'django' -%> + '<%= @appmodule %>', +<% end -%> ), } diff --git a/templates/inifile.erb b/templates/inifile.erb new file mode 100644 index 00000000..47ae7138 --- /dev/null +++ b/templates/inifile.erb @@ -0,0 +1,8 @@ +# this file is managed by puppet +<%- @config.sort.map do |section,conf| -%> +[<%= section -%>] +<%- conf.sort.map do |key,value| -%> +<%= key %> = <%= value %> +<%- end -%> + +<%- end -%> diff --git a/tests/gunicorn.pp b/tests/gunicorn.pp index c081fd64..8880b54c 100644 --- a/tests/gunicorn.pp +++ b/tests/gunicorn.pp @@ -11,4 +11,8 @@ dir => '/var/www/project1/current', bind => 'unix:/tmp/gunicorn.socket', environment => 'prod', + appmodule => 'app:app', + osenv => { 'DBHOST' => 'dbserver.example.com' }, + timeout => 30, + template => 'python/gunicorn.erb', } diff --git a/tests/pyvenv.pp b/tests/pyvenv.pp new file mode 100644 index 00000000..0f5bfff5 --- /dev/null +++ b/tests/pyvenv.pp @@ -0,0 +1,12 @@ +class { 'python': + pip=>false, + version=>'3', +} + +python::pyvenv { "/opt/uwsgi": +} + +python::pip { "uwsgi": + virtualenv => "/opt/uwsgi", + ensure => "latest" +} diff --git a/tests/requirements.pp b/tests/requirements.pp index a57093f0..90dccd8b 100644 --- a/tests/requirements.pp +++ b/tests/requirements.pp @@ -8,3 +8,9 @@ virtualenv => '/var/www/project1', proxy => 'http://proxy.domain.com:3128', } + +python::requirements { '/var/www/project1/requirements.txt': + virtualenv => 'system', + proxy => 'http://proxy.domain.com:3128', + timeout => 2400, +} diff --git a/tests/virtualenv.pp b/tests/virtualenv.pp index 190492c7..da5ab57c 100644 --- a/tests/virtualenv.pp +++ b/tests/virtualenv.pp @@ -9,4 +9,5 @@ version => 'system', requirements => '/var/www/project1/requirements.txt', proxy => 'http://proxy.domain.com:3128', + systempkgs => true, }