From 8ac188b8ca8a69a884cd8726c4f4d585c126b2f2 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Sun, 21 Jul 2013 12:23:30 -0600 Subject: [PATCH] Better virtualenv support. This commit: - Adds support for virtualenvwrapper. - Overrides the bash resource, adding support for running a bash block inside a virtualenv. - Adds configurable options for virtualenv. - Adds support for specifying virtualenvs with just the environment name, rather than the full path. --- attributes/default.rb | 36 +++++++++++++-- libraries/bash.rb | 61 +++++++++++++++++++++++++ providers/pip.rb | 10 +++- providers/virtualenv.rb | 63 ++++++++++++++++++++------ recipes/pip.rb | 2 +- recipes/source.rb | 11 +++-- recipes/virtualenv.rb | 30 +++++++++++- recipes/virtualenvwrapper.rb | 15 ++++++ resources/pip.rb | 9 ++-- resources/virtualenv.rb | 12 +++-- templates/default/virtualenv.sh | 19 ++++++++ templates/default/virtualenvwrapper.sh | 14 ++++++ 12 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 libraries/bash.rb create mode 100644 recipes/virtualenvwrapper.rb create mode 100644 templates/default/virtualenv.sh create mode 100644 templates/default/virtualenvwrapper.sh diff --git a/attributes/default.rb b/attributes/default.rb index 3e78072..9d0c261 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -18,25 +18,51 @@ # limitations under the License. # +# How should Python be installed, and where is it going to end up? default['python']['install_method'] = 'package' - if python['install_method'] == 'package' case platform when "smartos" - default['python']['prefix_dir'] = '/opt/local' + default['python']['prefix_dir'] = '/opt/local' else - default['python']['prefix_dir'] = '/usr' + default['python']['prefix_dir'] = '/usr' end else - default['python']['prefix_dir'] = '/usr/local' + default['python']['prefix_dir'] = '/usr/local' end +# Full path to the Python binary. default['python']['binary'] = "#{python['prefix_dir']}/bin/python" +# Information for building Python from source. +# These are ignored if Python is built from a package. default['python']['url'] = 'http://www.python.org/ftp/python' default['python']['version'] = '2.7.5' default['python']['checksum'] = '3b477554864e616a041ee4d7cef9849751770bc7c39adaf78a94ea145c488059' default['python']['configure_options'] = %W{--prefix=#{python['prefix_dir']}} - default['python']['distribute_script_url'] = 'http://python-distribute.org/distribute_setup.py' default['python']['distribute_option']['download_base'] = 'https://pypi.python.org/packages/source/d/distribute/' + +# Options for virtualenv. +default['python']['virtualenv']['version'] = nil # latest version +default['python']['virtualenv']['path'] = '/var/virtualenvs' + +# Who should own virtualenvs by default? +default['python']['virtualenv']['permissions']['owner'] = 'root' +default['python']['virtualenv']['permissions']['group'] = 'root' +default['python']['virtualenv']['permissions']['mode'] = 02775 + +# What are the default virtualenv settings? +# Note: On `prompt`, "$1" will be substituted with the name of the virtualenv +# being created. +default['python']['virtualenv']['options']['prompt'] = '($1)' +default['python']['virtualenv']['options']['python'] = "python#{node['python']['version'].split('.')[0...2].join('.')}" + +# Where should the script that sets up the appropriate environment +# variables for the above options live? +default['python']['virtualenv']['profile'] = '/etc/profile.d/virtualenv.sh' + +# Settings for virtualenvwrapper. +default['python']['virtualenv']['wrapper']['filename'] = '/usr/local/bin/virtualenvwrapper.sh' +default['python']['virtualenv']['wrapper']['profile'] = '/etc/profile.d/virtualenvprofile.sh' +default['python']['virtualenv']['wrapper']['version'] = nil # latest diff --git a/libraries/bash.rb b/libraries/bash.rb new file mode 100644 index 0000000..ae55588 --- /dev/null +++ b/libraries/bash.rb @@ -0,0 +1,61 @@ +require 'chef/resource/bash' +require 'chef/provider/script' + + +class Chef + class Resource + class Bash #< ::Chef::Resource::Bash + def initialize(name, run_context=nil) + super + @resource_name = :bash + @interpreter = 'bash' + @provider = Chef::Provider::Bash + @virtualenv = nil + end + + def virtualenv(arg=nil) + set_or_return( + :virtualenv, + arg, + :kind_of => String + ) + end + end + end +end + + +class Chef + class Provider + class Bash < ::Chef::Provider::Script + def initialize(new_resource, run_context) + # If there's a virtualenv specified, ensure that the code + # is run within that virtualenv. + if new_resource.virtualenv + # Determine the virtualenv, with the full path. + # Note about the syntax here: The `node` variable isn't available + # until the superclass constructor runs. However, I need to do this + # before that time because I want to ensure that my modification + # to the `code` attribute sticks. + virtualenv = new_resource.virtualenv + unless virtualenv.include?('/') + virtualenv = "#{run_context.node['python']['virtualenv']['path']}/#{virtualenv}" + end + + # Update the code block to ensure that our virtualenv + # is properly sourced before the block runs. + full_code = <<-EOF +source #{virtualenv}/bin/activate +#{new_resource.code} +EOF + + # Set the code block on the resource. + new_resource.code(full_code) + end + + # Now run the superclass constructor. + super + end + end + end +end diff --git a/providers/pip.rb b/providers/pip.rb index e7010d2..7d7f3e5 100644 --- a/providers/pip.rb +++ b/providers/pip.rb @@ -31,7 +31,8 @@ def whyrun_supported? # refactoring into core chef easy action :install do - # If we specified a version, and it's not the current version, move to the specified version + # If we specified a version, and it's not the current version, + # move to the specified version if new_resource.version != nil && new_resource.version != current_resource.version install_version = new_resource.version # If it's not installed at all, install it @@ -159,7 +160,12 @@ def pip_cmd(subcommand, version='') # this allows PythonPip to work with Chef::Resource::Package def which_pip(nr) if (nr.respond_to?("virtualenv") && nr.virtualenv) - ::File.join(nr.virtualenv,'/bin/pip') + if nr.virtualenv.include?('/') + full_virtualenv = nr.virtualenv + else + full_virtualenv = "#{node['python']['virtualenv']['path']}/#{nr.virtualenv}" + end + ::File.join(full_virtualenv,'/bin/pip') elsif node['python']['install_method'].eql?("source") ::File.join(node['python']['prefix_dir'], "/bin/pip") else diff --git a/providers/virtualenv.rb b/providers/virtualenv.rb index dfd9f4e..e3950b0 100644 --- a/providers/virtualenv.rb +++ b/providers/virtualenv.rb @@ -22,31 +22,53 @@ require 'chef/mixin/language' include Chef::Mixin::ShellOut + def whyrun_supported? - true + return true end + action :create do + normalize(new_resource) + unless exists? - Chef::Log.info("Creating virtualenv #{new_resource} at #{new_resource.path}") - execute "#{virtualenv_cmd} --python=#{new_resource.interpreter} #{new_resource.options} #{new_resource.path}" do - user new_resource.owner if new_resource.owner - group new_resource.group if new_resource.group + # Build the command. + venv_command = "#{virtualenv_cmd} #{new_resource.path}/#{new_resource.name}" + if new_resource.interpreter + venv_command += " --python=#{new_resource.interpreter}" + end + if new_resource.prompt + venv_command += " --prompt=\"#{new_resource.prompt.sub('$1', new_resource.name)}\"" + end + if new_resource.options + venv_command += " #{new_resource.options}" + end + + # Execute the command, actually creating the virtualenv. + execute "Creating virtualenv #{new_resource}" do + command venv_command + group new_resource.group || node['python']['virtualenv']['permissions']['group'] + user new_resource.owner || node['python']['virtualenv']['permissions']['owner'] end new_resource.updated_by_last_action(true) end end action :delete do + normalize(new_resource) + + # We still need to delete with a direct `rm -rf`, becuase `rmvirtualenv` + # doesn't handle receipt in the same way that `mkvirtualenv` does. if exists? description = "delete virtualenv #{new_resource} at #{new_resource.path}" converge_by(description) do Chef::Log.info("Deleting virtualenv #{new_resource} at #{new_resource.path}") - FileUtils.rm_rf(new_resource.path) + FileUtils.rm_rf("#{new_resource.path}/#{new_resource.name}") end end end + def load_current_resource @current_resource = Chef::Resource::PythonVirtualenv.new(new_resource.name) @current_resource.path(new_resource.path) @@ -59,16 +81,31 @@ def load_current_resource @current_resource end -def virtualenv_cmd() - if node['python']['install_method'].eql?("source") - ::File.join(node['python']['prefix_dir'], "/bin/virtualenv") + +private +def normalize(nr) + # The way that virtualenvs are specified has evolved; + # ensure that they are normalized to the new attributes format. + if nr.name.include?('/') + nr.path = nr.name.split('/')[0...-1].join('/') + nr.name = nr.name.split('/')[-1] + end + return nr +end + + +def virtualenv_cmd + if node['python']['install_method'].eql?('source') + ::File.join(node['python']['prefix_dir'], '/bin/virtualenv') else - "virtualenv" + 'virtualenv' end end -private + def exists? - ::File.exist?(current_resource.path) && ::File.directory?(current_resource.path) \ - && ::File.exists?("#{current_resource.path}/bin/activate") + path = "#{new_resource.path}/#{new_resource.name}" + return ::File.exists?(path) && \ + ::File.directory?(path) && \ + ::File.exists?("#{path}/bin/activate") end diff --git a/recipes/pip.rb b/recipes/pip.rb index caec0ae..e1de8dd 100644 --- a/recipes/pip.rb +++ b/recipes/pip.rb @@ -39,7 +39,7 @@ # https://bitbucket.org/ianb/pip/issue/104/pip-uninstall-on-ubuntu-linux remote_file "#{Chef::Config[:file_cache_path]}/distribute_setup.py" do source node['python']['distribute_script_url'] - mode "0644" + mode 0644 not_if { ::File.exists?(pip_binary) } end diff --git a/recipes/source.rb b/recipes/source.rb index eb8288d..0b03c50 100644 --- a/recipes/source.rb +++ b/recipes/source.rb @@ -23,10 +23,15 @@ configure_options = node['python']['configure_options'].join(" ") packages = value_for_platform_family( - "rhel" => ["openssl-devel","bzip2-devel","zlib-devel","expat-devel","db4-devel","sqlite-devel","ncurses-devel","readline-devel"], - "default" => ["libssl-dev","libbz2-dev","zlib1g-dev","libexpat1-dev","libdb-dev","libsqlite3-dev","libncursesw5-dev","libncurses5-dev","libreadline-dev","libsasl2-dev", "libgdbm-dev"] + "rhel" => ["openssl-devel","bzip2-devel","zlib-devel", + "expat-devel","db4-devel","sqlite-devel", + "ncurses-devel","readline-devel"], + "default" => ["libssl-dev","libbz2-dev","zlib1g-dev", + "libexpat1-dev","libdb-dev","libsqlite3-dev", + "libncursesw5-dev","libncurses5-dev", + "libreadline-dev","libsasl2-dev", "libgdbm-dev"], ) -# + packages.each do |dev_pkg| package dev_pkg end diff --git a/recipes/virtualenv.rb b/recipes/virtualenv.rb index 4c28f80..366001a 100644 --- a/recipes/virtualenv.rb +++ b/recipes/virtualenv.rb @@ -18,8 +18,34 @@ # limitations under the License. # -include_recipe "python::pip" +include_recipe 'python::pip' -python_pip "virtualenv" do +python_pip 'virtualenv' do action :install + version node['python']['virtualenv']['version'] +end + + +# Create the virtualenv home. +directory node['python']['virtualenv']['path'] do + action :create + group node['python']['virtualenv']['permissions']['group'] + mode node['python']['virtualenv']['permissions']['mode'] + owner node['python']['virtualenv']['permissions']['owner'] +end + + +# Create the virtualenv environment variable file. +# This file will need to be explicitly sourced by Chef whenever +# virtualenvs must be used, since the lack of a login shell means +# that it won't be done automatically. +# This functionality is handled for bash using the `python_bash` provider, +# which accepts a `virtualenv` attribute, and for `python_pip`, which also +# accepts a `virtualenv` attribute. +template "#{node['python']['virtualenv']['wrapper']['profile']}" do + action :create + group 'root' + mode 0755 + owner 'root' + source 'virtualenv.sh' end diff --git a/recipes/virtualenvwrapper.rb b/recipes/virtualenvwrapper.rb new file mode 100644 index 0000000..495c03c --- /dev/null +++ b/recipes/virtualenvwrapper.rb @@ -0,0 +1,15 @@ +# Install virtualenvwrapper. +python_pip 'virtualenvwrapper' do + action :install + version node['python']['virtualenv']['wrapper']['version'] +end + + +# Place the virtualenvwrapper environment variable file. +template "#{node['python']['virtualenv']['wrapper']['profile']}" do + action :create + group 'root' + mode 0755 + owner 'root' + source 'virtualenvwrapper.sh' +end diff --git a/resources/pip.rb b/resources/pip.rb index cccb224..7cad30d 100644 --- a/resources/pip.rb +++ b/resources/pip.rb @@ -27,10 +27,11 @@ def initialize(*args) @action = :install end + +attribute :group, :default => nil, :regex => Chef::Config[:group_valid_regex] +attribute :options, :default => '', :kind_of => String attribute :package_name, :kind_of => String, :name_attribute => true -attribute :version, :default => nil attribute :timeout, :default => 900 +attribute :user, :default => nil, :regex => Chef::Config[:user_valid_regex] +attribute :version, :default => nil attribute :virtualenv, :kind_of => String -attribute :user, :regex => Chef::Config[:user_valid_regex] -attribute :group, :regex => Chef::Config[:group_valid_regex] -attribute :options, :kind_of => String, :default => '' diff --git a/resources/virtualenv.rb b/resources/virtualenv.rb index dcb282c..9ff5310 100644 --- a/resources/virtualenv.rb +++ b/resources/virtualenv.rb @@ -27,8 +27,12 @@ def initialize(*args) @action = :create end -attribute :path, :kind_of => String, :name_attribute => true -attribute :interpreter, :default => 'python' -attribute :owner, :regex => Chef::Config[:user_valid_regex] -attribute :group, :regex => Chef::Config[:group_valid_regex] + +attribute :group, :default => nil, :regex => Chef::Config[:group_valid_regex] +attribute :interpreter, :default => node['python']['virtualenv']['options']['python'] +attribute :name, :kind_of => String, :name_attribute => true +attribute :owner, :default => nil, :regex => Chef::Config[:user_valid_regex] attribute :options, :kind_of => String +attribute :path, :default => node['python']['virtualenv']['path'], :kind_of => String +attribute :prefix, :default => node['python']['virtualenv']['options']['prefix'], :kind_of => String +attribute :prompt, :default => node['python']['virtualenv']['options']['prompt'] diff --git a/templates/default/virtualenv.sh b/templates/default/virtualenv.sh new file mode 100644 index 0000000..ce4d76b --- /dev/null +++ b/templates/default/virtualenv.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# ---------------------------------------------------------------- +# -- This profile script sets virtualenv settings. -- +# -- -- +# -- The contents of a this script are generated by Chef. -- +# -- Do not change them unless you are *sure* you know what -- +# -- you're doing. -- +# ---------------------------------------------------------------- + +# Use distribute rather than setuptools. +export VIRTUALENV_USE_DISTRIBUTE=1 + +# Set the default Python interpreter to use for virtualenvs. +export VIRTUALENV_PYTHON='<%= node['python']['virtualenv']['options']['python'] %>' + +# Make it such that all new virtualenvs that are created +# get a yellow prompt with a trailing space, rather than the default. +export VIRTUALENV_PROMPT='<%= node['python']['virtualenv']['options']['prompt'] %>' + diff --git a/templates/default/virtualenvwrapper.sh b/templates/default/virtualenvwrapper.sh new file mode 100644 index 0000000..5ff39c6 --- /dev/null +++ b/templates/default/virtualenvwrapper.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# ---------------------------------------------------------------- +# -- This profile script sets virtualenv settings. -- +# -- -- +# -- The contents of a this script are generated by Chef. -- +# -- Do not change them unless you are *sure* you know what -- +# -- you're doing. -- +# ---------------------------------------------------------------- + +# Include virtualenvwrapper. +. <%= node['python']['virtualenv']['wrapper']['filename'] %> + +# Ensure that virtualenvs are created in a consistent location. +export WORKON_HOME=<%= node['python']['virtualenv']['path'] %>