diff --git a/Berksfile.lock b/Berksfile.lock new file mode 100644 index 0000000..1898d09 --- /dev/null +++ b/Berksfile.lock @@ -0,0 +1,24 @@ +DEPENDENCIES + app + path: . + metadata: true + play2 + git: https://github.com/Originate/cookbooks.git + revision: a660e2104202a36e65d39ebc7b4842e82cd64f01 + rel: play2 + +GRAPH + app (0.1.0) + play2 (~> 0.1.0) + artifact (1.10.3) + windows (~> 1.8.0) + aws (2.4.0) + chef_handler (1.1.6) + java (1.14.0) + aws (>= 0.0.0) + windows (>= 0.0.0) + play2 (0.1.0) + artifact (~> 1.10.3) + java (~> 1.14.0) + windows (1.8.10) + chef_handler (>= 0.0.0) diff --git a/app/.kitchen.yml b/app/.kitchen.yml new file mode 100644 index 0000000..8e31207 --- /dev/null +++ b/app/.kitchen.yml @@ -0,0 +1,15 @@ +--- +driver: + name: vagrant + +provisioner: + name: chef_solo + +platforms: + - name: ubuntu-12.04 + - name: centos-6.4 + +suites: + - name: default + run_list: + attributes: diff --git a/app/Berksfile b/app/Berksfile new file mode 100644 index 0000000..77917d8 --- /dev/null +++ b/app/Berksfile @@ -0,0 +1,5 @@ +source "https://supermarket.getchef.com" + +metadata + +cookbook "play2", git: "https://github.com/Originate/cookbooks.git", rel: "play2" diff --git a/app/Berksfile.lock b/app/Berksfile.lock new file mode 100644 index 0000000..1898d09 --- /dev/null +++ b/app/Berksfile.lock @@ -0,0 +1,24 @@ +DEPENDENCIES + app + path: . + metadata: true + play2 + git: https://github.com/Originate/cookbooks.git + revision: a660e2104202a36e65d39ebc7b4842e82cd64f01 + rel: play2 + +GRAPH + app (0.1.0) + play2 (~> 0.1.0) + artifact (1.10.3) + windows (~> 1.8.0) + aws (2.4.0) + chef_handler (1.1.6) + java (1.14.0) + aws (>= 0.0.0) + windows (>= 0.0.0) + play2 (0.1.0) + artifact (~> 1.10.3) + java (~> 1.14.0) + windows (1.8.10) + chef_handler (>= 0.0.0) diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md new file mode 100644 index 0000000..dad46fa --- /dev/null +++ b/app/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +Initial release of app diff --git a/app/Gemfile b/app/Gemfile new file mode 100644 index 0000000..dc26e3c --- /dev/null +++ b/app/Gemfile @@ -0,0 +1,18 @@ +source 'https://rubygems.org' + +gem 'berkshelf' + +# Uncomment these lines if you want to live on the Edge: +# +# group :development do +# gem "berkshelf", github: "berkshelf/berkshelf" +# gem "vagrant", github: "mitchellh/vagrant", tag: "v1.6.3" +# end +# +# group :plugins do +# gem "vagrant-berkshelf", github: "berkshelf/vagrant-berkshelf" +# gem "vagrant-omnibus", github: "schisamo/vagrant-omnibus" +# end + +gem 'test-kitchen' +gem 'kitchen-vagrant' diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..8ce2314 --- /dev/null +++ b/app/LICENSE @@ -0,0 +1,3 @@ +Copyright (C) 2014 YOUR_NAME + +All rights reserved - Do Not Redistribute diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..643c2d6 --- /dev/null +++ b/app/README.md @@ -0,0 +1,42 @@ +# app-cookbook + +TODO: Enter the cookbook description here. + +## Supported Platforms + +TODO: List your supported platforms. + +## Attributes + + + + + + + + + + + + + + +
KeyTypeDescriptionDefault
['app']['bacon']Booleanwhether to include bacontrue
+ +## Usage + +### app::default + +Include `app` in your node's `run_list`: + +```json +{ + "run_list": [ + "recipe[app::default]" + ] +} +``` + +## License and Authors + +Author:: YOUR_NAME () diff --git a/app/Thorfile b/app/Thorfile new file mode 100644 index 0000000..b23ee16 --- /dev/null +++ b/app/Thorfile @@ -0,0 +1,12 @@ +# encoding: utf-8 + +require 'bundler' +require 'bundler/setup' +require 'berkshelf/thor' + +begin + require 'kitchen/thor_tasks' + Kitchen::ThorTasks.new +rescue LoadError + puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI'] +end diff --git a/app/Vagrantfile b/app/Vagrantfile new file mode 100644 index 0000000..6d4a9d9 --- /dev/null +++ b/app/Vagrantfile @@ -0,0 +1,88 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +Vagrant.require_version ">= 1.5.0" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + # All Vagrant configuration is done here. The most common configuration + # options are documented and commented below. For a complete reference, + # please see the online documentation at vagrantup.com. + + config.vm.hostname = "app-berkshelf" + + # Set the version of chef to install using the vagrant-omnibus plugin + config.omnibus.chef_version = :latest + + # Every Vagrant virtual environment requires a box to build off of. + # If this value is a shorthand to a box in Vagrant Cloud then + # config.vm.box_url doesn't need to be specified. + config.vm.box = "chef/ubuntu-14.04" + + # The url from where the 'config.vm.box' box will be fetched if it + # is not a Vagrant Cloud box and if it doesn't already exist on the + # user's system. + # config.vm.box_url = "https://vagrantcloud.com/chef/ubuntu-14.04/version/1/provider/virtualbox.box" + + # Assign this VM to a host-only network IP, allowing you to access it + # via the IP. Host-only networks can talk to the host machine as well as + # any other machines on the same network, but cannot be accessed (through this + # network interface) by any external networks. + config.vm.network :private_network, type: "dhcp" + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider :virtualbox do |vb| + # # Don't boot with headless mode + # vb.gui = true + # + # # Use VBoxManage to customize the VM. For example to change memory: + # vb.customize ["modifyvm", :id, "--memory", "1024"] + # end + # + # View the documentation for the provider you're using for more + # information on available options. + + # The path to the Berksfile to use with Vagrant Berkshelf + # config.berkshelf.berksfile_path = "./Berksfile" + + # Enabling the Berkshelf plugin. To enable this globally, add this configuration + # option to your ~/.vagrant.d/Vagrantfile file + config.berkshelf.enabled = true + + # An array of symbols representing groups of cookbook described in the Vagrantfile + # to exclusively install and copy to Vagrant's shelf. + # config.berkshelf.only = [] + + # An array of symbols representing groups of cookbook described in the Vagrantfile + # to skip installing and copying to Vagrant's shelf. + # config.berkshelf.except = [] + + config.vm.provision :chef_solo do |chef| + chef.json = { + mysql: { + server_root_password: 'rootpass', + server_debian_password: 'debpass', + server_repl_password: 'replpass' + } + } + + chef.run_list = [ + "recipe[app::default]" + ] + end +end diff --git a/app/chefignore b/app/chefignore new file mode 100644 index 0000000..138a808 --- /dev/null +++ b/app/chefignore @@ -0,0 +1,94 @@ +# Put files/directories that should be ignored in this file when uploading +# or sharing to the community site. +# Lines that start with '# ' are comments. + +# OS generated files # +###################### +.DS_Store +Icon? +nohup.out +ehthumbs.db +Thumbs.db + +# SASS # +######## +.sass-cache + +# EDITORS # +########### +\#* +.#* +*~ +*.sw[a-z] +*.bak +REVISION +TAGS* +tmtags +*_flymake.* +*_flymake +*.tmproj +.project +.settings +mkmf.log + +## COMPILED ## +############## +a.out +*.o +*.pyc +*.so +*.com +*.class +*.dll +*.exe +*/rdoc/ + +# Testing # +########### +.watchr +.rspec +spec/* +spec/fixtures/* +test/* +features/* +Guardfile +Procfile + +# SCM # +####### +.git +*/.git +.gitignore +.gitmodules +.gitconfig +.gitattributes +.svn +*/.bzr/* +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +cookbooks/* +tmp + +# Cookbooks # +############# +CONTRIBUTING +CHANGELOG* + +# Strainer # +############ +Colanderfile +Strainerfile +.colander +.strainer + +# Vagrant # +########### +.vagrant +Vagrantfile + +# Travis # +########## +.travis.yml diff --git a/app/metadata.json b/app/metadata.json new file mode 100644 index 0000000..f4b6ec2 --- /dev/null +++ b/app/metadata.json @@ -0,0 +1 @@ +{"name":"app","description":"Installs/Configures app","long_description":"Installs/Configures app","maintainer":"Antonio Morales","maintainer_email":"antonio.morales@originate.com","license":"All rights reserved","platforms":{},"dependencies":{"play2":"~> 0.1.0"},"recommendations":{},"suggestions":{},"conflicting":{},"providing":{},"replacing":{},"attributes":{},"groupings":{},"recipes":{},"version":"0.1.0"} \ No newline at end of file diff --git a/app/metadata.rb b/app/metadata.rb new file mode 100644 index 0000000..3185286 --- /dev/null +++ b/app/metadata.rb @@ -0,0 +1,10 @@ +name 'app' +maintainer 'Antonio Morales' +maintainer_email 'antonio.morales@originate.com' +license 'All rights reserved' +description 'Installs/Configures app' +long_description 'Installs/Configures app' +version '0.1.0' + +depends "play2", "~> 0.1.0" # add a dependnecy on Originate's play2 cookbook + diff --git a/app/recipes/default.rb b/app/recipes/default.rb new file mode 100644 index 0000000..726e774 --- /dev/null +++ b/app/recipes/default.rb @@ -0,0 +1,8 @@ +# +# Cookbook Name:: app +# Recipe:: default +# +# Copyright (C) 2014 YOUR_NAME +# +# All rights reserved - Do Not Redistribute +# diff --git a/app/recipes/deploy.rb b/app/recipes/deploy.rb new file mode 100644 index 0000000..8485118 --- /dev/null +++ b/app/recipes/deploy.rb @@ -0,0 +1,11 @@ +# app/recipes/deploy.rb +# +# Cookbook Name:: app +# Recipe:: deploy +# + +# This deploys the application +opsworks_play2 do + app "app" + deploy_data node[:deploy][:app] +end diff --git a/app/recipes/setub.rb b/app/recipes/setub.rb new file mode 100644 index 0000000..233c8f1 --- /dev/null +++ b/app/recipes/setub.rb @@ -0,0 +1,8 @@ +# app/recipes/setup.rb +# +# Cookbook Name:: app +# Recipe:: setup +# + +# This installs the play framework +include_recipe "play2::setup" diff --git a/artifact/CHANGELOG.md b/artifact/CHANGELOG.md new file mode 100644 index 0000000..0e19736 --- /dev/null +++ b/artifact/CHANGELOG.md @@ -0,0 +1,105 @@ +## v.1.10.2 + +* [#105](https://github.com/RiotGames/artifact-cookbook/pull/105) Fixed an edge case due to skip_manifest_check and actually writing the manifest file. +* [#101](https://github.com/RiotGames/artifact-cookbook/pull/101) Files downloaded from S3 should now be written using binary mode. +* [#103](https://github.com/RiotGames/artifact-cookbook/pull/103) When Chef runs fail, your password should not be exposed by the Chef::Artifact::NexusConfiguration object. + +## v1.10.1 + +* [#98] Pass the environment around fix nil access + +## v1.10.0 + +* [#90] Adds support for all regions to S3 +* [#89] New syntax for cusomizable Nexus configurations - see README +* [#91] Changes to logging to be less verbose - run in DEBUG to see all the old messaging +* [#93] Add customizable Nexus configuration support to artifact_package resource +* [#94] Fix skip_manifest_check and failed deploy race condition +* [#95] Remove activesupport dependency, bringing the method internal + +## v1.9.0 + +* [#85] Allows basic auth to be used for nexus artifact retrieval. + +## v1.8.1 + +* [#86] Repackage to get around tar issues. No changes. + +## v1.8.0 + +* [#84] Add artifact_package resource. + +## v1.7.1 + +* [#80] Fixes support for S3 and Ubuntu. Thanks to @ephess. + +## v1.7.0 + +* [#71] Added support for using S3 as an artifact deployment source. + +## v1.6.0 + +* [#70] Added a new LWRP, artifact\_file which wraps remote_file with some retry logic for corrupt downloads. Also uses the configured Nexus server to check Nexus downloads. +* Use artifact\_file for downloading HTTP or Nexus artifacts in artifact\_deploy. +* [#28] Add a new attribute for deleting the currently deployed artifact when a force deploy is issued. Useful for local iteration on a changing artifact with the same version. +* [#60] Add retries to execute resources for tar extraction. +* [#66], [#67] Cache the Encrypted Data Bag Item for Nexus. Looks for an environment-named data bag item, then "\_wildcard", and finally "nexus" for backwards compatibility. +* Support RSpec testing of the Library files. +* [#65] Support Test-Kitchen + +## v1.5.0 + +* Add a new attribute for skipping the manifest creation and checking for an artifact. Useful for large artifacts. + +## v1.4.0 + +* Add Windows support. +* Add a new attribute for removing a top level directory from the extracted zip file. + +## v1.3.1 + +* Fix a bug where Nokogiri was still used. + +## v1.3.0 + +* Use a newer nexus_cli gem which removes the requirement of installed libxml and libxslt packages. + +## v1.2.0 + +Bug Fixes +* [#50] Now actually SHA1 hashing the files themselves as opposed to hashing the String of the path to the file. +* [#52] Manifest generation now ignores symlinked files and directories. + +## v1.1.2 + +Bug Fixes +* case statement for extract_artifact! was not matching '.tar.gz' files correctly. + +## v1.1.1 + +Bug Fixes +* [#47] Regex was matching some special characters like '-'. + +## v1.1.0: + +Major Improvements +* New, simpler API for Chef::Artifact.get_actual_version. +* Added an ssl_verify attribute to the resource to help facilitate communications with Nexus servers that have invalid SSL certs. + +Bug Fixes +* [#45] Add a better check to ensure we don't redownload artifacts we already have. +* [#42] Deleting previous versions now uses Chef resources and is hopefully a bit more clear. +* [#35] Throw an error if the resource's name attribute has whitespace. +* Symlinks are now created in the symlink_it_up! method using recipe_eval. This helps ensure a clearer picture of the flow during the Chef run. +* Better logging throughout. + +## v1.0.0: + +Major Improvements +* Entirely new :deploy action flow. Please see the flowchart on the readme for a greater explanation. +* New :pre_seed action. This action will setup directories and download a the configured artifact. + +Bug Fixes +* [#37] Remove the circular dependency on the nexus-cookbook. +* [#33] No longer default to not verifying SSL connections when using the nexus-cli gem. +* [#29] Better handling of various types of archives. Now supports tar, tgz, bz, and zips. diff --git a/artifact/Gemfile b/artifact/Gemfile new file mode 100644 index 0000000..96445ef --- /dev/null +++ b/artifact/Gemfile @@ -0,0 +1,9 @@ +source "http://rubygems.org" + +gem 'berkshelf', '~> 1.4.0' +gem 'thor-foodcritic', '~> 0.1.2' +gem 'test-kitchen', git: 'git@github.com:opscode/test-kitchen.git' +gem 'kitchen-vagrant', :group => :integration +gem 'rspec' +gem 'chef', '~> 10.18' +gem 'aws-sdk', '= 1.15.0' \ No newline at end of file diff --git a/artifact/Gemfile.lock b/artifact/Gemfile.lock new file mode 100644 index 0000000..ebd30d9 --- /dev/null +++ b/artifact/Gemfile.lock @@ -0,0 +1,218 @@ +GIT + remote: git@github.com:opscode/test-kitchen.git + revision: b1bab8eec464e3871928b4b6dbbe54417bd3697f + specs: + test-kitchen (1.0.0.dev) + celluloid + mixlib-shellout + net-scp + net-ssh + pry + safe_yaml (~> 0.9.3) + thor + +GEM + remote: http://rubygems.org/ + specs: + activesupport (4.0.0) + i18n (~> 0.6, >= 0.6.4) + minitest (~> 4.2) + multi_json (~> 1.3) + thread_safe (~> 0.1) + tzinfo (~> 0.3.37) + addressable (2.3.5) + akami (1.2.0) + gyoku (>= 0.4.0) + nokogiri (>= 1.4.0) + atomic (1.1.10) + aws-sdk (1.15.0) + json (~> 1.4) + nokogiri (< 1.6.0) + uuidtools (~> 2.1) + berkshelf (1.4.6) + activesupport (>= 3.2.0) + addressable + celluloid (>= 0.14.0) + chozo (>= 0.6.1) + faraday (>= 0.8.5) + hashie (>= 2.0.2) + json (>= 1.5.0) + minitar + mixlib-config (~> 1.1) + mixlib-shellout (~> 1.1) + multi_json (~> 1.5) + retryable + ridley (~> 0.12.4) + solve (>= 0.4.2) + thor (~> 0.18.0) + yajl-ruby + builder (3.2.2) + bunny (0.7.9) + celluloid (0.14.1) + timers (>= 1.0.0) + chef (10.20.0) + bunny (>= 0.6.0, < 0.8.0) + erubis + highline (>= 1.6.9) + json (~> 1.7.6, >= 1.4.4) + mixlib-authentication (>= 1.3.0) + mixlib-cli (>= 1.1.0) + mixlib-config (>= 1.1.2) + mixlib-log (>= 1.3.0) + mixlib-shellout + moneta (< 0.7.0) + net-ssh (~> 2.6) + net-ssh-multi (~> 1.1.0) + ohai (>= 0.6.0) + rest-client (>= 1.0.4, < 1.7.0) + treetop (~> 1.4.9) + uuidtools + yajl-ruby (~> 1.1) + chozo (0.6.1) + activesupport (>= 3.2.0) + hashie (>= 2.0.2) + multi_json (>= 1.3.0) + coderay (1.0.9) + diff-lcs (1.2.4) + erubis (2.7.0) + faraday (0.8.8) + multipart-post (~> 1.2.0) + ffi (1.9.0) + foodcritic (2.2.0) + erubis + gherkin (~> 2.11.7) + nokogiri (~> 1.5.4) + treetop (~> 1.4.10) + yajl-ruby (~> 1.1.0) + gherkin (2.11.8) + multi_json (~> 1.3) + gssapi (1.0.3) + ffi (>= 1.0.1) + gyoku (1.1.0) + builder (>= 2.1.2) + hashie (2.0.5) + highline (1.6.19) + httpclient (2.2.0.2) + httpi (0.9.7) + rack + i18n (0.6.4) + ipaddress (0.8.0) + json (1.7.7) + kitchen-vagrant (0.11.0) + test-kitchen (~> 1.0.0.beta.1) + little-plugger (1.1.3) + logging (1.6.2) + little-plugger (>= 1.1.3) + method_source (0.8.2) + mime-types (1.23) + minitar (0.5.4) + minitest (4.7.5) + mixlib-authentication (1.3.0) + mixlib-log + mixlib-cli (1.3.0) + mixlib-config (1.1.2) + mixlib-log (1.6.0) + mixlib-shellout (1.2.0) + moneta (0.6.0) + multi_json (1.7.7) + multipart-post (1.2.0) + net-http-persistent (2.9) + net-scp (1.1.2) + net-ssh (>= 2.6.5) + net-ssh (2.6.8) + net-ssh-gateway (1.2.0) + net-ssh (>= 2.6.5) + net-ssh-multi (1.1) + net-ssh (>= 2.1.4) + net-ssh-gateway (>= 0.99.0) + nokogiri (1.5.10) + nori (1.1.5) + ohai (6.18.0) + ipaddress + mixlib-cli + mixlib-config + mixlib-log + mixlib-shellout + systemu + yajl-ruby + polyglot (0.3.3) + pry (0.9.12.2) + coderay (~> 1.0.5) + method_source (~> 0.8) + slop (~> 3.4) + rack (1.5.2) + rest-client (1.6.7) + mime-types (>= 1.16) + retryable (1.3.3) + ridley (0.12.4) + addressable + celluloid (~> 0.14.0) + chozo (>= 0.6.0) + erubis + faraday (>= 0.8.4) + hashie (>= 2.0.2) + mixlib-authentication (>= 1.3.0) + mixlib-config (>= 1.1.0) + mixlib-log (>= 1.3.0) + mixlib-shellout (>= 1.1.0) + net-http-persistent (>= 2.8) + net-ssh + retryable + solve (>= 0.4.4) + winrm (~> 1.1.0) + rspec (2.14.1) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rspec-core (2.14.4) + rspec-expectations (2.14.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.2) + rubyntlm (0.1.1) + safe_yaml (0.9.4) + savon (0.9.5) + akami (~> 1.0) + builder (>= 2.1.2) + gyoku (>= 0.4.0) + httpi (~> 0.9) + nokogiri (>= 1.4.0) + nori (~> 1.0) + wasabi (~> 1.0) + slop (3.4.6) + solve (0.8.0) + systemu (2.5.2) + thor (0.18.1) + thor-foodcritic (0.1.2) + foodcritic + thor + thread_safe (0.1.2) + atomic + timers (1.1.0) + treetop (1.4.14) + polyglot + polyglot (>= 0.3.1) + tzinfo (0.3.37) + uuidtools (2.1.4) + wasabi (1.0.0) + nokogiri (>= 1.4.0) + winrm (1.1.2) + gssapi (~> 1.0.0) + httpclient (~> 2.2.0.2) + logging (~> 1.6.1) + nokogiri (~> 1.5.0) + rubyntlm (~> 0.1.1) + savon (= 0.9.5) + uuidtools (~> 2.1.2) + yajl-ruby (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + aws-sdk (= 1.15.0) + berkshelf (~> 1.4.0) + chef (~> 10.18) + kitchen-vagrant + rspec + test-kitchen! + thor-foodcritic (~> 0.1.2) diff --git a/artifact/LICENSE b/artifact/LICENSE new file mode 100644 index 0000000..8879847 --- /dev/null +++ b/artifact/LICENSE @@ -0,0 +1,13 @@ +Copyright 2013, Riot Games + +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/artifact/README.md b/artifact/README.md new file mode 100644 index 0000000..17d09e5 --- /dev/null +++ b/artifact/README.md @@ -0,0 +1,504 @@ +# Artifact cookbook + +Provides your cookbooks with the Artifact Deploy LWRP + +# Requirements + +* Chef 10 +* Vagrant + +# Platforms + +* CentOS +* Fedora +* Windows >= 6.0 + * Windows Vista + * Windows 2008 R2 + * Windows 7 + +# Vagrant + +With Vagrant 1.1, there is no longer a Vagrant RubyGem to install. Instead, follow the instructions on the [VagrantUp](http://docs.vagrantup.com/v2/installation/index.html) documentation pages. + +# Resources / Providers + +## artifact_deploy + +Deploys a collection of build artifacts packaged into a tar ball. Artifacts are extracted from +the package and managed in a deploy directory in the same fashion you've seen in the Opscode +deploy resource or Capistrano's default deploy strategy. + +### Actions +Action | Description | Default +------- |------------- |--------- +deploy | Deploy the artifact package | Yes +pre_seed | Pre-seed the artifact package | + +### Attributes +Attribute | Description |Type | Default +--------- |------------- |----- |-------- +artifact_name | Name of the artifact package to deploy | String | name +artifact_location | URL, S3 path, local path, or Maven identifier of the artifact package to download | String | +artifact_checksum | The SHA256 checksum of the artifact package that is being downloaded | String | +deploy_to | Deploy directory where releases are stored and linked | String | +version | Version of the artifact being deployed | String | +owner | Owner of files created and modified | String | +group | Group of files created and modified | String | +environment | An environment hash used by resources within the provider | Hash | Hash.new +symlinks | A hash that maps files in the shared directory to their paths in the current release | Hash | Hash.new +shared_directories | Directories to be created in the shared folder | Array | %w{ log pids } +force | Forcefully deploy an artifact even if the artifact has already been deployed | Boolean | false +should_migrate | Notify the provider if it should perform application migrations | Boolean | false +keep | Specify a number of artifacts deployments to keep on disk | Integer | 2 +before_deploy | A proc containing resources to be executed before the deploy process begins | Proc | +before_extract | A proc containing resources to be executed before the artifact package is extracted | Proc | +after_extract | A proc containing resources to be executed after the artifac package is extracted | Proc | +before_symlink | A proc containing resources to be executed before the symlinks are created | Proc | +after_symlink | A proc containing resources to be executed after the symlinks are created | Proc | +configure | A proc containing resources to be executed to configure the artifact package | Proc | +before_migrate | A proc containing resources to be executed before the migration Proc | Proc | +migrate | A proc containing resources to be executed during the migration stage | Proc | +after_migrate | A proc containing resources to be executed after the migration Proc | Proc | +restart | A proc containing resources to be executed at the end of a successful deploy | Proc | +after_deploy | A proc containing resources to be executed after the deploy process ends | Proc | +remove_top_level_directory | Deletes a top level directory from the extracted zip file | Boolean | false +skip_manifest_check | Skips the manifest check for idempotency when the version attribute is not changing | Boolean | false +remove_on_force | Removes the current version directory contents when force is set | Boolean | false +nexus_configuration | Accepts an object that can customize the Nexus server connection information | Chef::Artifact::NexusConfiguration | Chef::Artifact::NexusConfiguration.from_data_bag + +### Deploy Flow, the Manifest, and Procs + +The deploy flow is outlined in the Artifact Deploy flow chart below. + +![Artifact Deploy](https://raw.github.com/RiotGames/artifact-cookbook/gh-pages/images/ArtifactDeployFlow.png) + +For a more detailed flow of what happens when we check with `deploy?`, see the [Manifest Differences Flow chart.](http://riotgames.github.com/artifact-cookbook/images/ManifestDifferencesFlow.png) + +The 'happy-path' of this flow is the default path when an artifact has already been deploy - there will be no need to +execute many of the Procs. That being said, there are a few 'choice' paths through the flow where a Proc may affect the +flow. + +There are two checks in the artifact deploy flow where a *manifest* check is executed - at the beginning, before the *before_deploy* proc, +and just after the *configure* proc (and after the *migrate* procs). When the latter check returns true, the *restart* proc will execute. + +The *manifest* is a YAML file with a mapping of files in the deploy path to their SHA1 checksum. For example: + +``` +/srv/artifact_test/releases/2.0.68/log4j.xml: 96be5753fbf845e30b643fa04008f2c4fe6956a7 +/srv/artifact_test/releases/2.0.68/readme.txt: fcb8d816b062565930f19f9bdb954f5ac43c5039 +/srv/artifact_test/releases/2.0.68/my-artifact.jar: 42ad63cc883afad010573d3d8eea4e5a4011e5d4 +``` + +There are numerous Procs placed throughout the flow of the artifact_deploy resource. They are meant to give the user many different +ways to configure the artifact and execute resources during the flow. Some good examples include executing a resource to stop a service +in the *before_deploy* proc, or placing configuration files in the deployed artifact during the *configure* proc. + +**Please note** the *before_deploy*, *configure*, and *after_deploy* procs are executed on every Chef run. It is recommended that any *template* +(or configuration changing resource calls) take place within those procs. In particular, the *configure* proc was added for this very purpose. Following +this pattern will ensure that the templates will change, and the *restart* proc will execute (perhaps restarting the service the configured artifact provides +in order to pick up the configuration changes). + +Procs can also utilize the internal methods of the provider class, because they are evaluated inside of the instance of the provider class. For example: + +``` +artifact_deploy "artifact_test" do + # omitted for brevity + configure Proc.new { + # release_path is an attr_reader on the @release_path variable + template "#{release_path}/conf/config.properties" do + source "config.properties.erb" + variables(:config => config) + end + } +end +``` + +## artifact_file + +Downloads a file from a provided location and then verifies that the integrity of the file is intact. Artifact files from Nexus +will check with the Nexus Server to verify the SHA1 of the downloaded file. Artifact files from an HTTP or S3 source will either use +the provided SHA256 checksum to verify integrity or skip the check if no checksum is given. + +### Actions +Action | Description | Default +------- |------------- |--------- +create | Download the artifact file | Yes + +### Attributes +Attribute | Description |Type | Default +--------- |------------- |----- |-------- +path | The path to download the artifact to | String | name +location | The location to the artifact file. Either a nexus identifier, S3 path or URL | String | +checksum | The SHA256 checksum for verifying URL downloads. Not used when location is Nexus | String | +owner | Owner of the downloaded file | String | +group | Group of the downloaded file | String | +download_retries | The number of times to attempt to download the file if it fails its integrity check | Integer | 1 + +### Downloading files using artifact_file + +In its simplest state, the artifact_file resource is a wrapper for the remote_file resource for Nexus and URL locations. The key addition is retry logic and integrity checking +for the downloaded files. Below is a brief description of the logic flow for the resource: + +* Download the file using remote_file resource. +* Check the file's integrity + * Is it from the Nexus? + * Check the SHA1 of the downloaded file against Nexus Server's SHA1. Returns false if they are not equal. + * Not from Nexus - Is the checksum attribute defined for the resource? + * If defined - Check the SHA256 of the downloaded file against the checksum attribute. Returns false if they are not equal. + * If not defined - log a message and return true. + +When the logic returns true, the downloaded file is considered good and the resource will exit. When the logic above returns false, the downloaded file is considered +corrupt and an attempt will be made to download the file again. The number of retries can be controlled with the `download_retries` attribute. + + +## artifact_package + +Downloads a file with artifact_file, then calls the package resource to install it. + +### Actions +Action | Description | Default +------- |------------- |--------- +install | Download and install the artifact file | Yes + +### Attributes +Attribute | Description |Type | Default +--------- |------------- |----- |-------- +name | The name of the package you'll be installing | String | name +location | The location to the artifact file. Either a nexus identifier, S3 path or URL | String | +checksum | The SHA256 checksum for verifying URL downloads. Not used when location is Nexus | String | +owner | Owner of the downloaded file | String | +group | Group of the downloaded file | String | +download_retries | The number of times to attempt to download the file if it fails its integrity check | Integer | 1 + + +# Documentation + +The RDocs for the deploy.rb provider can be found under the [Top Level Namespace](http://riotgames.github.com/artifact-cookbook/doc/top-level-namespace.html) page +for this repository. + +## Nexus Usage + +By default, deploying an artifact from a Nexus repository requires an [encrypted data bag](http://wiki.opscode.com/display/chef/Encrypted+Data+Bags) that contains +the credentials for your Nexus repository. + + knife data bag create artifact _wildcard -c --secret-file= + +Your data bag should look like the following: + + { + "id": "_wildcard", + "nexus": { + "username": "nexus_user", + "password": "nexus_user_password", + "url": "http://nexus.yourcompany.com:8081/nexus/", + "repository": "your_repository" + } + } + +After your encrypted data bag is setup you can use Maven identifiers +as your `artifact_location` attribute. A Maven identifier is shown as a colon-separated string +that includes three elements - groupId:artifactId:extension - ex. "com.my.artifact:my-artifact:tgz". +If many environments share the same configuration, you can provide environment specific configuration in +separate data_bag items: + + knife data bag create artifact production -c --secret-file= + + { + "id": "production", + "nexus": { + "username": "nexus_production_user", + "password": "nexus_production_user_password", + "url": "http://nexus.yourcompany.com:8081/nexus/", + "repository": "your_repository" + } + } + + knife data bag create artifact development -c --secret-file= + + { + "id": "development", + "nexus": { + "username": "nexus_dev_user", + "password": "nexus_dev_user_password", + "url": "http://nexus-dev.yourcompany.com:8081/nexus/", + "repository": "your_repository" + } + } + +To further customize your Nexus usage, you can use the new `nexus_configuration` attribute. To do so, create a new `Chef::Artifact::NexusConfiguration` object, passing it +the customized parameters - url, repository, username (defaults to nil), password (defaults to nil), ssl_verify (defaults to true). Then pass that object to the `artifact_deploy` resource. For example: + + nexus_configuration_object = Chef::Artifact::NexusConfiguration("http://nexus-url", "snapshots", "username", "password") + + artifact_deploy "my-artifact" do + version "latest" + artifact_location "com.foo:my-artifact:tgz" + nexus_configuration nexus_configuration_object + deploy_to "/opt/my-artifact" + owner "artifact" + group "artifact" + end + +To access a Nexus repository anonymously, simply omit the username and password from the construction of the `Chef::Artifact::NexusConfiguration` object. + +## S3 Usage + +S3 can be used as a source of an archive. The location path must be in the form ```s3://s3-endpoint/bucket-name/path/to/archive.tar.gz```. You can provide AWS credentials in the data_bag, +or if you are running on EC2 and are using IAM Instance Roles - you may omit the credentials and use the Instance Role. Alternatively, if the credentials are available on the +environment they will be used from there (more information on the Environment variable keys an be found ). + +The S3 endpoints are documented here . + +If you want to retrieve an object from an S3 bucket in the US-Standard region - use the following format: ```s3://s3.amazonaws.com/bucket-name/path/to/archive.tar.gz```. + +If you want to retrieve and object from a bucket in the US West (Oregon) Region region ```s3://s3-us-west-2.amazonaws.com/bucket-name/path/to/archive.tar.gz``` or EU (Ireland) Region: ```s3://s3-eu-west-1.amazonaws.com/bucket-name/path/to/archive.tar.gz``` + +This is example IAM policy to get an artifact from S3. Note that this policy will limit permissions to just the files contained under the ```deploys/``` path. +If you wish to keep them in the root of your bucket, just omit the ```deploys/``` portion and put ```/*```: + + { + "Statement": [ + { + "Sid": "Stmt1357328135477", + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::/deploys/*", + "arn:aws:s3:::" + ] + } + ] + } + +If you wish to provide your AWS credentials in a data_bag, the format is: + + { + "id": "_wildcard", + "aws": { + "access_key_id": "my_access_key", + "secret_access_key": "my_secret_access_key" + } + } + +Your data_bag can contain both ```nexus``` and ```aws``` configuration. + +# Examples + +##### Deploying a Rails application + + artifact_deploy "pvpnet" do + version "1.0.0" + artifact_location "https://artifacts.location.riotgames.com/pvpnet-1.0.0.tar.gz" + deploy_to "/srv/pvpnet" + owner "riot" + group "riot" + environment { 'RAILS_ENV' => 'production' } + shared_directories %w{ data log pids system vendor_bundle assets } + + before_deploy Proc.new { + bluepill_service 'pvpnet-unicorn' do + action :stop + end + } + + before_migrate Proc.new { + template "#{shared_path}/database.yml" do + source "database.yml.erb" + owner node[:merlin][:owner] + group node[:merlin][:group] + mode "0644" + variables( + :environment => environment, + :options => database_options + ) + end + + execute "bundle install --local --path=vendor/bundle --without test development cucumber --binstubs" do + environment { 'RAILS_ENV' => 'production' } + user "riot" + group "riot" + end + } + + migrate Proc.new { + execute "bundle exec rake db:migrate" do + cwd release_path + environment { 'RAILS_ENV' => 'production' } + user "riot" + group "riot" + end + } + + after_migrate Proc.new { + ruby_block "remove_run_migrations" do + block do + Chef::Log.info("Migrations were run, removing role[pvpnet_run_migrations]") + node.run_list.remove("role[pvpnet_run_migrations]") + end + end + } + + configure Proc.new { + template "/srv/pvpnet/current/config.properties" do + source "config.properties.erb" + owner 'riot' + group 'riot' + variables(:database_config => node[:pvpnet_cookbook][:database_config]) + end + } + + restart Proc.new { + bluepill_service 'pvpnet-unicorn' do + action :restart + end + } + + keep 2 + should_migrate (node[:pvpnet][:should_migrate] ? true : false) + force (node[:pvpnet][:force_deploy] ? true : false) + action :deploy + end + +##### Deploying the latest from Nexus (Changed in > 1.0.0) + + artifact_deploy "my-artifact" do + version "latest" + artifact_location "com.foo:my-artifact:tgz" + deploy_to "/opt/my-artifact" + owner "artifact" + group "artifact" + + before_extract Proc.new { + service "my-artifact" do + action :stop + end + } + + configure Proc.new { + template "#{release_path}/conf/config.properties" do + source "config.properties.erb" + owner "artifact" + group "artifact" + variables(:config => node[:my_artifact_cookbook][:config]) + end + } + + restart Proc.new { + service "my-artifact" do + action :start + end + } + end + +##### Deploying an artifact from Amazon S3 + + artifact_deploy "my-artifact" do + version "1.0.0" + artifact_location "s3://s3.amazonaws.com/my-website-deployments/deploys/my-artifact-1.0.0.tgz" + deploy_to "/srv/my-artifact" + owner node[:artifact_owner] + group node[:artifact_group] + symlinks({ + "log" => "log" + }) + end + +##### Configuring an artifact_deploy that may need to change over many Chef runs + + artifact_deploy "my-artifact" do + version "1.0.0" + artifact_location "http://www.fooo.com/my-artifact-1.0.0.tgz" + deploy_to "/srv/my-artifact" + owner node[:artifact_owner] + group node[:artifact_group] + symlinks({ + "log" => "log" + }) + force node[:force_deploy] + end + +##### Using artifact_file to download a file from a URL + + artifact_file "/tmp/my-artifact.tgz" do + location "http://www.my-website.com/my-artifact-1.0.0.tgz" + owner "me" + group "mes" + action :create + end + +#### Using artifact_file to download a file from Nexus + + artifact_file "/tmp/my-artifact.tgz" do + location "com.test:my-artifact:tgz:1.0.0" + owner "me" + group "mes" + action :create + end + + Configuring your resource in this manner will allow you to ensure it can always change when you need it to. In other words, + configuring the `force` attribute to a node attribute, will allow you to change some of the more finer grained aspects of the + resource. For example, when force is true, you can also change the value of owner and group to remap the deployed artifact to + a new permissions scheme. + +##### Using artifact_file to download a file from an S3 bucket + + artifact_file "/tmp/my-artifact.tgz" do + location "s3://s3.amazonaws.com/my-website-deployments/deploys/my-artifact-1.0.0.tgz" + owner "me" + group "mes" + checksum "fcb188ed37d41ff2cbf1a52d3a11bfde666e036b5c7ada1496dc1d53dd6ed5dd" + action :create + end + +##### Using artifact_package to install an rpm from a website + + artifact_package "tomcat" do + location "http://my-website/tomcat-5.0.rpm" + owner "me" + group "mes" + action :install + end + +##### Using artifact_package to install an rpm from Nexus + + artifact_package "tomcat" do + location "com.rpm:tomcat:rpm:6.0.0" + owner "me" + group "mes" + action :install + end + +# Testing + +A sample cookbook is available in `fixtures`. You can package it with mkartifact.sh, and +upload it to Nexus as artifact_cookbook:test:1.2.3:tgz. + +Set the artifact_test_location and artifact_test_version environment variables when running +vagrant to change how they'll be provisioned. Default is 1.2.3 from a file URL. + +* artifact_test_location=artifact_cookbook:test:1.2.3:tgz artifact_test_version=1.2.3 bundle exec vagrant + +# Releasing + +1. Install the prerequisite gems + + $ gem install chef + $ gem install thor + +2. Increment the version number in the metadata.rb file + +3. Run the Thor release task to create a tag and push to the community site + + $ thor release + +# License and Author + +Author:: Jamie Winsor ()
+Author:: Kyle Allan () + +Copyright 2013, Riot Games + +See LICENSE for license details diff --git a/artifact/Thorfile b/artifact/Thorfile new file mode 100644 index 0000000..c75fd06 --- /dev/null +++ b/artifact/Thorfile @@ -0,0 +1,91 @@ +# encoding: utf-8 + +require 'bundler' +require 'bundler/setup' +require 'thor/foodcritic' +require 'berkshelf/thor' +require 'chef/cookbook/metadata' + +class Default < Thor + class_option :verbose, + :type => :boolean, + :aliases => "-v", + :default => false + + method_option :knife_config, + :type => :string, + :aliases => "-c", + :desc => "Path to your knife configuration file", + :default => "~/.chef/knife.rb" + desc "release", "Create a tag from the version specific in the metadata.rb and push to the community site" + def release + unless clean? + say "There are files that need to be committed first.", :red + exit 1 + end + + tag_version { + publish_cookbook(options) + } + end + + private + + def clean? + sh_with_excode("git diff --exit-code")[1] == 0 + end + + def current_version + metadata = Chef::Cookbook::Metadata.new + metadata.from_file(source_root.join("metadata.rb").to_s) + metadata.version + end + + def publish_cookbook(options) + cmd = "knife cookbook site share artifact \"Utilities\" -o #{source_root.join("..")} -c #{options[:knife_config]}" + cmd << " -V" if options[:verbose] + system cmd + end + + def tag_version + sh "git tag -a -m \"Version #{current_version}\" #{current_version}" + say "Tagged: #{current_version}", :green + yield if block_given? + sh "git push --tags" + rescue => e + say "Untagging: #{current_version} due to error", :red + sh_with_excode "git tag -d #{current_version}" + say e, :red + exit 1 + end + + def source_root + Pathname.new File.dirname(File.expand_path(__FILE__)) + end + + def sh(cmd, dir = source_root, &block) + out, code = sh_with_excode(cmd, dir, &block) + code == 0 ? out : raise(out.empty? ? "Running `#{cmd}` failed. Run this command directly for more detailed output." : out) + end + + def sh_with_excode(cmd, dir = source_root, &block) + cmd << " 2>&1" + outbuf = '' + + Dir.chdir(dir) { + outbuf = `#{cmd}` + if $? == 0 + block.call(outbuf) if block + end + } + + [ outbuf, $? ] + end +end + +begin + require 'kitchen/thor_tasks' + Kitchen::ThorTasks.new +rescue LoadError + puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV['CI'] +end diff --git a/artifact/Vagrantfile b/artifact/Vagrantfile new file mode 100644 index 0000000..4be2edf --- /dev/null +++ b/artifact/Vagrantfile @@ -0,0 +1,29 @@ +VERSION = ENV["artifact_test_version"] || "1.2.3" +LOCATION = ENV["artifact_test_location"] || "/vagrant/fixtures/artifact_test-#{VERSION}.tgz" + +Vagrant.configure("2") do |config| + + config.vm.hostname = "artifact-berkshelf" + + config.berkshelf.enabled = true + + config.vm.box = "Berkshelf-CentOS-6.3-x86_64-minimal" + config.vm.box_url = "https://dl.dropbox.com/u/31081437/Berkshelf-CentOS-6.3-x86_64-minimal.box" + + config.vm.network :private_network, ip: "192.168.33.10" + + config.vm.provision :chef_solo do |chef| + chef.json = { + :artifact_test => { + :version => VERSION, + :location => LOCATION + } + } + + chef.data_bags_path = "./fixtures/databags/" + + chef.run_list = [ + "recipe[artifact_test::default]" + ] + end +end diff --git a/artifact/chefignore b/artifact/chefignore new file mode 100644 index 0000000..6ffb53a --- /dev/null +++ b/artifact/chefignore @@ -0,0 +1,49 @@ +# Put files/directories that should be ignored in this file. +# Lines that start with '# ' are comments. + +## OS +.DS_Store +Icon? +nohup.out + +## EDITORS +\#* +.#* +*~ +*.sw[a-z] +*.bak +REVISION +TAGS* +tmtags +*_flymake.* +*_flymake +*.tmproj +.project +.settings +mkmf.log + +## COMPILED +a.out +*.o +*.pyc +*.so + +## OTHER SCM +*/.bzr/* +*/.hg/* +*/.svn/* + +## Don't send rspecs up in cookbook +.watchr +.rspec +spec/* +spec/fixtures/* +features/* +fixtures/* + +## SCM +.gitignore + +# Berkshelf +Berksfile +Berksfile.lock \ No newline at end of file diff --git a/artifact/libraries/activesupport_hash_diff.rb b/artifact/libraries/activesupport_hash_diff.rb new file mode 100644 index 0000000..3a485d7 --- /dev/null +++ b/artifact/libraries/activesupport_hash_diff.rb @@ -0,0 +1,28 @@ +# All credit given to Activesupport +# https://github.com/rails/rails/blob/v3.2.13/activesupport/lib/active_support/core_ext/hash/diff.rb +# +# Copyright (c) 2005-2011 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +class Hash + def diff(h2) + self.dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| self.has_key?(k) }) + end +end diff --git a/artifact/libraries/chef_artifact.rb b/artifact/libraries/chef_artifact.rb new file mode 100644 index 0000000..4f92296 --- /dev/null +++ b/artifact/libraries/chef_artifact.rb @@ -0,0 +1,278 @@ +class Chef + module Artifact + DATA_BAG = "artifact".freeze + WILDCARD_DATABAG_ITEM = "_wildcard".freeze + DATA_BAG_NEXUS = 'nexus'.freeze + DATA_BAG_AWS = 'aws'.freeze + + module File + + # Returns true if the given file is a symlink. + # + # @param path [String] the path to the file to test + # + # @return [Boolean] + def symlink?(path) + if windows? + require 'chef/win32/file' + return Chef::ReservedNames::Win32::File.symlink?(path) + end + ::File.symlink?(path) + end + + # Returns the value of the readlink method. + # + # @param path [String] the path to a symlink + # + # @return [String] the path that the symlink points to + def readlink(path) + if windows? + require 'chef/win32/file' + return Chef::ReservedNames::Win32::File.readlink(path) + end + ::File.readlink(path) + end + + # Generates a command to execute that either uses the Unix cp + # command or the Windows copy command. + # + # @param source [String] the file to copy + # @param destination [String] the path to copy the source to + # + # @return [String] a useable command to copy a file + def copy_command_for(source, destination) + if windows? + %Q{copy "#{source}" "#{destination}"}.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) + else + "cp -r #{source} #{destination}" + end + end + + # @return [Fixnum or nil] + def windows? + Chef::Platform.windows? + end + end + + class << self + include Chef::Artifact::File + + # Loads the encrypted data bag item and returns credentials + # for the environment or for a default key. + # + # @param environment [String] the environment + # @param source [String] the deployment source to load configuration for + # + # @return [Chef::DataBagItem] the data bag item + def data_bag_config_for(environment, source) + data_bag_item = if Chef::Config[:solo] + Chef::DataBagItem.load(DATA_BAG, WILDCARD_DATABAG_ITEM) rescue {} + else + encrypted_data_bag_for(environment, DATA_BAG) + end + + # support new format + return data_bag_item[source] if data_bag_item.has_key?(source) + + # backwards compatible for old data bag formats using nexus + return data_bag_item if DATA_BAG_NEXUS == source + + # no config found for source + {} + end + + # Downloads a file to disk from an Amazon S3 bucket + # + # @param node [Chef::Node] the node + # @param source_file [String] a s3 url that represents the artifact in the form: s3://// + # @param destination_file [String] a path to download the artifact to + # + def retrieve_from_s3(node, source_file, destination_file) + begin + require 'aws-sdk' + config = data_bag_config_for(node.chef_environment, DATA_BAG_AWS) + s3_endpoint, bucket_name, object_name = parse_s3_url(source_file) + + if config.empty? + AWS.config(:s3 => { :endpoint => s3_endpoint }) + else + AWS.config(:access_key_id => config['access_key_id'], + :secret_access_key => config['secret_access_key'], + :s3 => { :endpoint => s3_endpoint }) + end + + object = get_s3_object(bucket_name, object_name) + + Chef::Log.debug("Downloading #{object_name} from S3 bucket #{bucket_name}") + ::File.open(destination_file, 'wb') do |file| + object.read do |chunk| + file.write(chunk) + end + Chef::Log.debug("File #{destination_file} is #{file.size} bytes on disk") + end + rescue URI::InvalidURIError + Chef::Log.warn("Expected an S3 URL but found #{source_file}") + raise + end + end + + # Parse a source url to retrieve the specific parts required to interact with the object on S3. + # + # @example + # s3_endpoint, bucket_name, object_name = Chef::Artifact.parse_s3_url('s3://s3.amazonaws.com/my-bucket/my-file.txt') + # + # @param source_url [String] Source url to parse + # + # @return [Array] An array containing the S3 endpoint, Bucket Name and Object Name + def parse_s3_url(source_url) + protocol, s3_endpoint, bucket_and_object = URI.split(source_url).compact + path_parts = bucket_and_object[1..-1].split('/') + bucket_name = path_parts[0] + object_name = path_parts[1..-1].join('/') + [s3_endpoint, bucket_name, object_name] + end + + # Given a bucket and object name - fetches the object from S3 + # + # @example + # object = Chef::Artifact.get_s3_object('my-bucket', 'my-file.txt') + # + # @param bucket_name [String] Name of the S3 bucket + # @param object_name [String] Name of the S3 object + # + # @return [AWS::S3::S3Object] An S3 Object + def get_s3_object(bucket_name, object_name) + s3_client = AWS::S3.new() + bucket = s3_client.buckets[bucket_name] + raise S3BucketNotFoundError.new(bucket_name) unless bucket.exists? + + object = bucket.objects[object_name] + raise S3ArtifactNotFoundError.new(bucket_name, object_name) unless object.exists? + object + end + + # Returns true when the artifact is believed to be from a + # Nexus source. + # + # @param location [String] the artifact_location + # + # @return [Boolean] true when the location is a colon-separated value + def from_nexus?(location) + !from_http?(location) && location.split(":").length > 2 + end + + # Returns true when the artifact is believed to be from an + # S3 bucket. + # + # @param location [String] the artifact_location + # + # @return [Boolean] true when the location matches s3 + def from_s3?(location) + location_of_type(location, 's3') + end + + # Returns true when the artifact is believed to be from an + # http source. + # + # @param location [String] the artifact_location + # + # @return [Boolean] true when the location matches http or https. + def from_http?(location) + location_of_type(location, %w(http https)) + end + + # Returns true when the location URI scheme matches the type + # + # @param location [String] the location URI to check + # @param uri_type [Array] list of URI types to check + # + # @return [Boolean] true when the location matches the given URI type + def location_of_type(location, uri_type) + not (location =~ URI::regexp(uri_type)).nil? + end + + # Convenience method for determining whether a String is "latest" + # + # @param version [String] the version of the configured artifact to check + # + # @return [Boolean] true when version matches (case-insensitive) "latest" + def latest?(version) + version.casecmp("latest") == 0 + end + + # Returns the currently deployed version of an artifact given that artifacts + # installation directory by reading what directory the 'current' symlink + # points to. + # + # @param deploy_to_dir [String] the directory where an artifact is installed + # + # @example + # Chef::Artifact.get_current_deployed_version("/opt/my_deploy_dir") => "2.0.65" + # + # @return [String] the currently deployed version of the given artifact + def get_current_deployed_version(deploy_to_dir) + + current_dir = ::File.join(deploy_to_dir, "current") + if ::File.exists?(current_dir) + ::File.basename(readlink(current_dir)) + end + end + + # Looks for the given data bag in the cache and if not found, will load a + # data bag item named for the chef_environment, '_wildcard', or the old + # 'nexus' value. + # + # @param environment [String] the environment + # @param data_bag [String] the data bag to load + # + # @return [Chef::Mash] the data bag item in Mash form + def encrypted_data_bag_for(environment, data_bag) + @encrypted_data_bags = {} unless @encrypted_data_bags + + if encrypted_data_bags[data_bag] + return get_from_data_bags_cache(data_bag) + else + data_bag_item = encrypted_data_bag_item(data_bag, environment) + data_bag_item ||= encrypted_data_bag_item(data_bag, WILDCARD_DATABAG_ITEM) + data_bag_item ||= encrypted_data_bag_item(data_bag, "nexus") + data_bag_item ||= {} + @encrypted_data_bags[data_bag] = data_bag_item + return data_bag_item + end + end + + # @return [Hash] + def encrypted_data_bags + @encrypted_data_bags + end + + # Loads an entry from the encrypted_data_bags class variable. + # + # @param data_bag [String] the data bag to find + # + # @return [type] [description] + def get_from_data_bags_cache(data_bag) + encrypted_data_bags[data_bag] + end + + # Loads an EncryptedDataBagItem from the Chef server and + # turns it into a Chef::Mash, giving it indifferent access. Returns + # nil when a data bag item is not found. + # + # @param data_bag [String] + # @param data_bag_item [String] + # + # @raise [Chef::Artifact::DataBagEncryptionError] when the data bag cannot be decrypted + # or transformed into a Mash for some reason (Chef 10 vs Chef 11 data bag changes). + # + # @return [Chef::Mash] + def encrypted_data_bag_item(data_bag, data_bag_item) + Mash.from_hash(Chef::EncryptedDataBagItem.load(data_bag, data_bag_item).to_hash) + rescue Net::HTTPServerException => e + nil + rescue NoMethodError + raise DataBagEncryptionError.new + end + end + end +end diff --git a/artifact/libraries/chef_artifact_errors.rb b/artifact/libraries/chef_artifact_errors.rb new file mode 100644 index 0000000..be70268 --- /dev/null +++ b/artifact/libraries/chef_artifact_errors.rb @@ -0,0 +1,77 @@ +class Chef + module Artifact + class ArtifactError < StandardError; end + + class DataBagNotFound < ArtifactError + attr_reader :data_bag_key + + def initialize(data_bag_key) + @data_bag_key = data_bag_key + end + + def message + "[artifact] Unable to locate the Artifact data bag '#{DATA_BAG}' or data bag item '#{data_bag_key}' for your environment." + end + end + + class EnvironmentNotFound < ArtifactError + attr_reader :data_bag_key + attr_reader :environment + + def initialize(data_bag_key, environment) + @data_bag_key = data_bag_key + @environment = environment + end + + def message + "[artifact] Unable to locate the Artifact data bag item '#{data_bag_key}' for your environment '#{environment}'." + end + end + + class DataBagEncryptionError < ArtifactError + def message + "[artifact] An error occured while decrypting the data bag item. Your secret key may be incorrect or you may be using Chef 11 to read a Chef 10 data bag." + end + end + + class ArtifactChecksumError < ArtifactError + def message + "[artifact] Downloaded file checksum does not match the provided checksum. Your download may be corrupted or your checksum may not be correct." + end + end + + class ArtifactDownloadError < ArtifactError + attr_accessor :message + + def initialize(message) + @message = message + end + end + + class S3BucketNotFoundError < ArtifactError + attr_reader :bucket_name + + def initialize(bucket_name) + @bucket_name = bucket_name + end + + def message + "[artifact] Unable to locate the S3 bucket: #{bucket_name}" + end + end + + class S3ArtifactNotFoundError < ArtifactError + attr_reader :bucket_name + attr_reader :object_path + + def initialize(bucket_name, object_path) + @bucket_name = bucket_name + @object_path = object_path + end + + def message + "[artifact] Unable to locate the artifact on S3 at the path #{bucket_name}/#{object_path}" + end + end + end +end diff --git a/artifact/libraries/chef_artifact_nexus.rb b/artifact/libraries/chef_artifact_nexus.rb new file mode 100644 index 0000000..8688df5 --- /dev/null +++ b/artifact/libraries/chef_artifact_nexus.rb @@ -0,0 +1,60 @@ +require 'rexml/document' + +class Chef + module Artifact + class Nexus + + attr_reader :node, :nexus_configuration + + def initialize(node, nexus_configuration) + require 'nexus_cli' + @node, @nexus_configuration = node, nexus_configuration + end + + def remote + @_remote ||= NexusCli::RemoteFactory.create(nexus_configuration.to_hash, nexus_configuration.ssl_verify) + end + + # Uses the provided parameters to make a call to the data bag + # configured Nexus server to have the server tell us what the + # actual version number is when 'latest' is given. + # + # @param coordinates [String] a colon-separated Maven identifier string that represents the artifact + # + # @example + # nexus.get_actual_version("com.myartifact:my-artifact:tgz:latest") => "2.0.5" + # nexus.get_actual_version("com.myartifact:my-artifact:tgz:1.0.1") => "1.0.1" + # + # @return [String] the version number that latest resolves to or the passed in value + def get_actual_version(coordinates) + version = coordinates.split(':')[3] + if Chef::Artifact.latest?(version) + REXML::Document.new(remote.get_artifact_info(coordinates)).elements["//version"].text + else + version + end + end + + # Downloads a file to disk from the configured Nexus server. + # + # @param source [String] a colon-separated Maven identified string that represents the artifact + # @param destination_dir [String] a path to download the artifact to + # + # @return [Hash] writes a file to disk and returns a Hash with + # information about that file. See NexusCli::ArtifactActions#pull_artifact. + def retrieve_from_nexus(source, destination_dir) + remote.pull_artifact(source, destination_dir) + end + + # Makes a call to Nexus and parses the returned XML to return + # the Nexus Server's stored SHA1 checksum for the given artifact. + # + # @param coordinates [String] a colon-separated Maven identifier that represents the artifact + # + # @return [String] the SHA1 entry for the artifact + def get_artifact_sha(coordinates) + REXML::Document.new(remote.get_artifact_info(coordinates)).elements["//sha1"].text + end + end + end +end diff --git a/artifact/libraries/chef_artifact_nexus_configuration.rb b/artifact/libraries/chef_artifact_nexus_configuration.rb new file mode 100644 index 0000000..2c2c227 --- /dev/null +++ b/artifact/libraries/chef_artifact_nexus_configuration.rb @@ -0,0 +1,43 @@ +class Chef + module Artifact + class NexusConfiguration + class << self + def from_data_bag + environment = unless Chef::Config[:solo] + Chef::Node.load(Chef::Config[:node_name]).chef_environment + else + nil + end + + config = Chef::Artifact.data_bag_config_for(environment, Chef::Artifact::DATA_BAG_NEXUS) + if config.nil? || config.empty? + Chef::Log.debug "No Data Bag found for NexusConfiguration." + nil + else + new(config['url'], config['repository'], config['username'], config['password']) + end + end + end + + attr_accessor :url, :repository, :username, :password, :ssl_verify + + def initialize(url, repository, username=nil, password=nil, ssl_verify=true) + @url, @repository, @username, @password, @ssl_verify = url, repository, username, password, ssl_verify + end + + alias_method :inspect_without_masking, :inspect + def inspect + self.inspect_without_masking.sub(/@password=".*"/, '@password="MASKED"') + end + + def to_hash + { + 'url' => url, + 'repository' => repository, + 'username' => username, + 'password' => password + } + end + end + end +end diff --git a/artifact/metadata.json b/artifact/metadata.json new file mode 100644 index 0000000..dd21379 --- /dev/null +++ b/artifact/metadata.json @@ -0,0 +1,35 @@ +{ + "name": "artifact", + "description": "Provides your cookbooks with the Artifact Deploy LWRP", + "long_description": "", + "maintainer": "Riot Games", + "maintainer_email": "kallan@riotgames.com", + "license": "Apache 2.0", + "platforms": { + "centos": ">= 0.0.0", + "redhat": ">= 0.0.0", + "fedora": ">= 0.0.0", + "ubuntu": ">= 0.0.0", + "windows": ">= 6.0" + }, + "dependencies": { + "windows": "~> 1.8.0" + }, + "recommendations": { + }, + "suggestions": { + }, + "conflicting": { + }, + "providing": { + }, + "replacing": { + }, + "attributes": { + }, + "groupings": { + }, + "recipes": { + }, + "version": "1.10.3" +} \ No newline at end of file diff --git a/artifact/providers/deploy.rb b/artifact/providers/deploy.rb new file mode 100644 index 0000000..8ef0ff9 --- /dev/null +++ b/artifact/providers/deploy.rb @@ -0,0 +1,637 @@ +# +# Cookbook Name:: artifact +# Provider:: deploy +# +# Author:: Jamie Winsor () +# Author:: Kyle Allan () +# +# Copyright 2013, Riot Games +# +# 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. +# +require 'digest' +require 'pathname' +require 'yaml' + +attr_reader :release_path +attr_reader :current_path +attr_reader :shared_path +attr_reader :artifact_cache +attr_reader :artifact_cache_version_path +attr_reader :manifest_file +attr_reader :previous_version_paths +attr_reader :previous_version_numbers +attr_reader :artifact_location +attr_reader :artifact_version +attr_reader :nexus_configuration_object +attr_reader :nexus_connection + +def load_current_resource + if Chef::Artifact.latest?(@new_resource.version) && Chef::Artifact.from_http?(@new_resource.artifact_location) + Chef::Application.fatal! "You cannot specify the latest version for an artifact when attempting to download an artifact using http(s)!" + end + + if @new_resource.name =~ /\s/ + Chef::Log.warn "Whitespace detected in resource name. Failing Chef run." + Chef::Application.fatal! "The name attribute for this resource is significant, and there cannot be whitespace. The preferred usage is to use the name of the artifact." + end + + if Chef::Artifact.from_nexus?(@new_resource.artifact_location) + chef_gem "nexus_cli" do + version "4.0.2" + end + + @nexus_configuration_object = new_resource.nexus_configuration + @nexus_connection = Chef::Artifact::Nexus.new(node, nexus_configuration_object) + group_id, artifact_id, extension = @new_resource.artifact_location.split(':') + @artifact_version = nexus_connection.get_actual_version([group_id, artifact_id, extension, @new_resource.version].join(':')) + @artifact_location = [group_id, artifact_id, extension, artifact_version].join(':') + elsif Chef::Artifact.from_s3?(@new_resource.artifact_location) + unless Chef::Artifact.windows? + case node['platform_family'] + when 'debian' + nokogiri_requirements = %W{gcc make libxml2 libxslt1.1 libxml2-dev libxslt1-dev} + when 'rhel' + nokogiri_requirements = %W{gcc make libxml2 libxslt libxml2-devel libxslt-devel} + else + Chef::Log.warn "Watch out, you might not be able to install the nokogiri gem!" + end + + nokogiri_requirements.each do |nokogiri_requirement| + package nokogiri_requirement do + action :nothing + end.run_action(:install) + end + end + + chef_gem "aws-sdk" do + version "1.11.0" + end + + @artifact_version = @new_resource.version + @artifact_location = @new_resource.artifact_location + else + @artifact_version = @new_resource.version + @artifact_location = @new_resource.artifact_location + end + + @release_path = get_release_path + @current_path = @new_resource.current_path + @shared_path = @new_resource.shared_path + @artifact_cache = ::File.join(@new_resource.artifact_deploys_cache_path, @new_resource.name) + @artifact_cache_version_path = ::File.join(artifact_cache, artifact_version) + @previous_version_paths = get_previous_version_paths + @previous_version_numbers = get_previous_version_numbers + @manifest_file = ::File.join(@release_path, "manifest.yaml") + @deploy = false + @skip_manifest_check = @new_resource.skip_manifest_check + @remove_on_force = @new_resource.remove_on_force + @nexus_configuration_object = @new_resource.nexus_configuration + @current_resource = Chef::Resource::ArtifactDeploy.new(@new_resource.name) + + @current_resource +end + +action :deploy do + delete_current_if_forcing! + setup_deploy_directories! + setup_shared_directories! + + @deploy = manifest_differences? + + retrieve_artifact! + + run_proc :before_deploy + + if deploy? + run_proc :before_extract + if new_resource.is_tarball + extract_artifact! + else + copy_artifact + end + run_proc :after_extract + + run_proc :before_symlink + symlink_it_up! + run_proc :after_symlink + end + + run_proc :configure + + if deploy? && new_resource.should_migrate + run_proc :before_migrate + run_proc :migrate + run_proc :after_migrate + end + + if deploy? || manifest_differences? || current_symlink_changing? + run_proc :restart + end + + recipe_eval do + if Chef::Artifact.windows? + # Needed until CHEF-3960 is fixed. + symlink_changing = current_symlink_changing? + execute "delete the symlink at #{new_resource.current_path}" do + command "rmdir #{new_resource.current_path}" + only_if {Chef::Artifact.symlink?(new_resource.current_path) && symlink_changing} + end + end + + link new_resource.current_path do + to release_path + owner new_resource.owner + group new_resource.group + end + end + + run_proc :after_deploy + + recipe_eval { write_manifest } unless skip_manifest_check? + delete_previous_versions! + + new_resource.updated_by_last_action(true) +end + +action :pre_seed do + setup_deploy_directories! + retrieve_artifact! +end + +# Extracts the artifact defined in the resource call. Handles +# a variety of 'tar' based files (tar.gz, tgz, tar, tar.bz2, tbz) +# and a few 'zip' based files (zip, war, jar). +# +# @return [void] +def extract_artifact! + recipe_eval do + case ::File.extname(cached_tar_path) + when /gz|tgz|tar|bz2|tbz/ + execute "extract_artifact!" do + command "tar xf #{cached_tar_path} -C #{release_path}" + user new_resource.owner + group new_resource.group + retries 2 + end + when /zip|war|jar/ + if Chef::Artifact.windows? + windows_zipfile release_path do + source cached_tar_path + overwrite true + retries 2 + end + else + package "unzip" + execute "extract_artifact!" do + command "unzip -q -u -o #{cached_tar_path} -d #{release_path}" + user new_resource.owner + group new_resource.group + retries 2 + end + end + else + Chef::Application.fatal! "Cannot extract artifact because of its extension. Supported types are [tar.gz tgz tar tar.bz2 tbz zip war jar]." + end + + # Working with artifacts that are packaged under an extra top level directory + # can be cumbersome. Remove it if a top level directory exists and the user + # says to + release_pathname = Pathname.new(release_path) + ruby_block "remove top level" do + block do + top_level_dir = release_pathname.children.first.to_s + ::FileUtils.mv(release_pathname.children.first.children, release_path) + ::FileUtils.rm_rf(top_level_dir) + end + only_if do + new_resource.remove_top_level_directory && + release_pathname.children.size == 1 && + release_pathname.children.first.directory? + end + end + end +end + +# Copies the artifact from its cached path to its release path. The cached path is +# the configured Chef::Config[:file_cache_path]/artifact_deploys +# +# @example +# cp /tmp/vagrant-chef-1/artifact_deploys/artifact_test/1.0.0/my-artifact /srv/artifact_test/releases/1.0.0 +# +# @return [void] +def copy_artifact + recipe_eval do + execute "copy artifact" do + command Chef::Artifact.copy_command_for(cached_tar_path, release_path) + user new_resource.owner + group new_resource.group + end + end +end + +# Returns the file path to the cached artifact the resource is installing. +# +# @return [String] the path to the cached artifact +def cached_tar_path + ::File.join(artifact_cache_version_path, artifact_filename) +end + +# Returns the filename of the artifact being installed when the LWRP +# is called. Depending on how the resource is called in a recipe, the +# value returned by this method will change. If Chef::Artifact.from_nexus?, return the +# concatination of "artifact_id-version.extension" otherwise return the +# basename of where the artifact is located. +# +# @example +# When: new_resource.artifact_location => "com.artifact:my-artifact:1.0.0:tgz" +# artifact_filename => "my-artifact-1.0.0.tgz" +# When: new_resource.artifact_location => "http://some-site.com/my-artifact.jar" +# artifact_filename => "my-artifact.jar" +# +# @return [String] the artifacts filename +def artifact_filename + if Chef::Artifact.from_nexus?(new_resource.artifact_location) + group_id, artifact_id, extension, version = artifact_location.split(":") + unless extension + extension = "jar" + end + "#{artifact_id}-#{version}.#{extension}" + else + ::File.basename(artifact_location) + end +end + +# Deletes the current version if and only if it is the same +# as the one to be installed, we are forcing, and remove_on_force is +# set. Only bad people will use this. +def delete_current_if_forcing! + return unless @new_resource.force + return unless remove_on_force? + return unless get_current_release_version == artifact_version || previous_version_numbers.include?(artifact_version) + + recipe_eval do + log "artifact_deploy[delete_current_if_forcing!] #{artifact_version} deleted because remove_on_force is true" do + level :info + end + + directory ::File.join(new_resource.deploy_to, 'releases', artifact_version) do + recursive true + action :delete + end + end +end + +# Deletes released versions of the artifact when the number of +# released versions exceeds the :keep value. +def delete_previous_versions! + recipe_eval do + versions_to_delete = [] + + keep = new_resource.keep + delete_first = total = get_previous_version_paths.length + + if total == 0 || total <= keep + true + else + delete_first -= keep + Chef::Log.info "artifact_deploy[delete_previous_versions!] is deleting #{delete_first} of #{total} old versions (keeping: #{keep})" + versions_to_delete = get_previous_version_paths.shift(delete_first) + end + + versions_to_delete.each do |version| + log "artifact_deploy[delete_previous_versions!] #{version.basename} deleted" do + level :info + end + + directory ::File.join(artifact_cache, version.basename) do + recursive true + action :delete + end + + directory ::File.join(new_resource.deploy_to, 'releases', version.basename) do + recursive true + action :delete + end + end + end +end + +private + + # A wrapper that adds debug logging for running a recipe_eval on the + # numerous Proc attributes defined for this resource. + # + # @param name [Symbol] the name of the proc to execute + # + # @return [void] + def run_proc(name) + proc = new_resource.send(name) + proc_name = name.to_s + Chef::Log.debug "artifact_deploy[run_proc::#{proc_name}] Determining whether to execute #{proc_name} proc." + if proc + Chef::Log.debug "artifact_deploy[run_proc::#{proc_name}] Beginning execution of #{proc_name} proc." + recipe_eval(&proc) + Chef::Log.debug "artifact_deploy[run_proc::#{proc_name}] Ending execution of #{proc_name} proc." + else + Chef::Log.debug "artifact_deploy[run_proc::#{proc_name}] Skipping execution of #{proc_name} proc because it was not defined." + end + end + + # Checks the various cases of whether an artifact has or has not been installed. If the artifact + # has been installed let #has_manifest_changed? determine the return value. + # + # @return [Boolean] + def manifest_differences? + if new_resource.force + Chef::Log.debug "artifact_deploy[manifest_differences?] Force attribute has been set for #{new_resource.name}." + Chef::Log.info "artifact_deploy[manifest_differences?] Installing version, #{artifact_version} for #{new_resource.name}." + return true + elsif get_current_release_version.nil? + Chef::Log.debug "artifact_deploy[manifest_differences?] No current version installed for #{new_resource.name}." + Chef::Log.info "artifact_deploy[manifest_differences?] Installing version, #{artifact_version} for #{new_resource.name}." + return true + elsif artifact_version != get_current_release_version && !previous_version_numbers.include?(artifact_version) + Chef::Log.debug "artifact_deploy[manifest_differences?] Currently installed version of artifact is #{get_current_release_version}." + Chef::Log.debug "artifact_deploy[manifest_differences?] Version #{artifact_version} for #{new_resource.name} has not already been installed." + Chef::Log.info "artifact_deploy[manifest_differences?] Installing version, #{artifact_version} for #{new_resource.name}." + return true + elsif artifact_version != get_current_release_version && previous_version_numbers.include?(artifact_version) + Chef::Log.info "artifact_deploy[manifest_differences?] Version #{artifact_version} of artifact has already been installed." + return has_manifest_changed? + elsif artifact_version == get_current_release_version + Chef::Log.info "artifact_deploy[manifest_differences?] Currently installed version of artifact is #{artifact_version}." + return has_manifest_changed? + end + end + + # Loads the saved manifest.yaml file and generates a new, current manifest. The + # saved manifest is then parsed through looking for files that may have been deleted, + # added, or modified. + # + # @return [Boolean] + def has_manifest_changed? + Chef::Log.debug "artifact_deploy[has_manifest_changed?] Loading manifest.yaml file from directory: #{release_path}" + begin + saved_manifest = YAML.load_file(::File.join(release_path, "manifest.yaml")) + rescue Errno::ENOENT + unless skip_manifest_check? + Chef::Log.warn "artifact_deploy[has_manifest_changed?] Cannot load manifest.yaml. It may have been deleted. Deploying." + return true + end + end + + if skip_manifest_check? + Chef::Log.debug "artifact_deploy[has_manifest_changed?] Skip Manifest Check attribute is true. Skipping manifest check." + return false + end + + current_manifest = generate_manifest(release_path) + Chef::Log.debug "artifact_deploy[has_manifest_changed?] Comparing saved manifest from #{release_path} with regenerated manifest from #{release_path}." + + differences = !saved_manifest.diff(current_manifest).empty? + if differences + Chef::Log.info "artifact_deploy[has_manifest_changed?] Saved manifest from #{release_path} differs from regenerated manifest. Deploying." + return true + else + Chef::Log.info "artifact_deploy[has_manifest_changed?] Saved manifest from #{release_path} is the same as regenerated manifest. Not Deploying." + return false + end + end + + # Checks the not-equality of the current_release_version against the version of + # the currently configured resource. Returns true when the current symlink will + # be changed to a different release of the artifact at the end of the resource + # call. + # + # @return [Boolean] + def current_symlink_changing? + get_current_release_version != ::File.basename(release_path) + end + + # @return [Boolean] the deploy instance variable + def deploy? + @deploy + end + + # @return [Boolean] the skip_manifest_check instance variable + def skip_manifest_check? + @skip_manifest_check + end + + # @return [Boolean] the remove_on_force instance variable + def remove_on_force? + @remove_on_force + end + + # @return [String] the current version the current symlink points to + def get_current_release_version + Chef::Artifact.get_current_deployed_version(new_resource.deploy_to) + end + + # Returns a path to the artifact being installed by + # the configured resource. + # + # @example + # When: + # new_resource.deploy_to = "/srv/artifact_test" and artifact_version = "1.0.0" + # get_release_path => "/srv/artifact_test/releases/1.0.0" + # + # @return [String] the artifacts release path + def get_release_path + ::File.join(new_resource.deploy_to, "releases", artifact_version) + end + + # Searches the releases directory and returns an Array of version folders. After + # rejecting the current release version from the Array, the array is sorted by mtime + # and returned. + # + # @return [Array] the mtime sorted array of currently installed versions + def get_previous_version_paths + versions = Dir[::File.join(new_resource.deploy_to, "releases", '**')].collect do |v| + Pathname.new(v) + end + + versions.reject! { |v| v.basename.to_s == get_current_release_version } + + versions.sort_by(&:mtime) + end + + # Convenience method for returning just the version numbers of + # the currently installed versions of the artifact. + # + # @return [Array] the currently installed version numbers + def get_previous_version_numbers + previous_version_paths.collect { |version| version.basename.to_s} + end + + # Creates directories and symlinks as defined by the symlinks + # attribute of the resource. + # + # @return [void] + def symlink_it_up! + recipe_eval do + new_resource.symlinks.each do |key, value| + Chef::Log.info "artifact_deploy[symlink_it_up!] Creating and linking #{new_resource.shared_path}/#{key} to #{release_path}/#{value}" + directory "#{new_resource.shared_path}/#{key}" do + owner new_resource.owner + group new_resource.group + mode '0755' + recursive true + end + + link "#{release_path}/#{value}" do + to "#{new_resource.shared_path}/#{key}" + owner new_resource.owner + group new_resource.group + end + end + end + end + + # Creates directories that are necessary for installing + # the artifact. + # + # @return [void] + def setup_deploy_directories! + recipe_eval do + [ artifact_cache_version_path, release_path, shared_path ].each do |path| + Chef::Log.info "artifact_deploy[setup_deploy_directories!] Creating #{path}" + directory path do + owner new_resource.owner + group new_resource.group + mode '0755' + recursive true + end + end + end + end + + # Creates directories that are defined in the shared_directories + # attribute of the resource. + # + # @return [void] + def setup_shared_directories! + recipe_eval do + new_resource.shared_directories.each do |dir| + Chef::Log.info "artifact_deploy[setup_shared_directories!] Creating #{shared_path}/#{dir}" + directory "#{shared_path}/#{dir}" do + owner new_resource.owner + group new_resource.group + mode '0755' + recursive true + end + end + end + end + + # Retrieves the configured artifact based on the + # artifact_location instance variable. + # + # @return [void] + def retrieve_artifact! + recipe_eval do + if Chef::Artifact.from_http?(new_resource.artifact_location) + Chef::Log.info "artifact_deploy[retrieve_artifact!] Retrieving artifact from #{artifact_location}" + retrieve_from_http + elsif Chef::Artifact.from_nexus?(new_resource.artifact_location) + Chef::Log.info "artifact_deploy[retrieve_artifact!] Retrieving artifact from Nexus using #{artifact_location}" + retrieve_from_nexus + elsif Chef::Artifact.from_s3?(new_resource.artifact_location) + Chef::Log.info "artifact_deploy[retrieve_artifact!] Retrieving artifact from S3 using #{artifact_location}" + retrieve_from_s3 + elsif ::File.exist?(new_resource.artifact_location) + Chef::Log.info "artifact_deploy[retrieve_artifact!] Retrieving artifact local path #{artifact_location}" + retrieve_from_local + else + Chef::Application.fatal! "artifact_deploy[retrieve_artifact!] Cannot retrieve artifact #{artifact_location}! Please make sure the artifact exists in the specified location." + end + end + end + + # Defines a resource call for downloading the remote artifact. + # + # @return [void] + def retrieve_from_http + artifact_file cached_tar_path do + location new_resource.artifact_location + owner new_resource.owner + group new_resource.group + checksum new_resource.artifact_checksum + action :create + end + end + + # Defines a artifact_file resource call to download an artifact from Nexus. + # + # @return [void] + def retrieve_from_nexus + artifact_file cached_tar_path do + location artifact_location + owner new_resource.owner + group new_resource.group + nexus_configuration nexus_configuration_object + action :create + end + end + + # Defines a artifact_file resource call to download an artifact from S3. + # + # @return [void] + def retrieve_from_s3 + artifact_file cached_tar_path do + location new_resource.artifact_location + owner new_resource.owner + group new_resource.group + checksum new_resource.artifact_checksum + action :create + end + end + + # Defines a resource call for a file already on the file system. + # + # @return [void] + def retrieve_from_local + execute "copy artifact from #{new_resource.artifact_location} to #{cached_tar_path}" do + command Chef::Artifact.copy_command_for(new_resource.artifact_location, cached_tar_path) + user new_resource.owner + group new_resource.group + only_if { !::File.exists?(cached_tar_path) || !FileUtils.compare_file(new_resource.artifact_location, cached_tar_path) } + end + end + + # Generates a manifest for all the files underneath the given files_path. SHA1 digests will be + # generated for all files under the given files_path with the exception of directories and the + # manifest.yaml file itself. + # + # @param files_path [String] a path to the files that a manfiest will be generated for + # + # @return [Hash] a mapping of file_path => SHA1 of that file + def generate_manifest(files_path) + Chef::Log.debug "artifact_deploy[generate_manifest] Generating manifest for files in #{files_path}" + files_in_release_path = Dir[::File.join(files_path, "**/*")].reject { |file| ::File.directory?(file) || file =~ /manifest.yaml/ || Chef::Artifact.symlink?(file) } + + {}.tap do |map| + files_in_release_path.each { |file| map[file] = Digest::SHA1.file(file).hexdigest } + end + end + + # Generates a manfiest Hash for the files under the release_path and + # writes a YAML dump of the created Hash to manifest_file. + # + # @return [String] a String of the YAML dumped to the manifest.yaml file + def write_manifest + manifest = generate_manifest(release_path) + Chef::Log.debug "artifact_deploy[write_manifest] Writing manifest.yaml file to #{manifest_file}" + ::File.open(manifest_file, "w") { |file| file.puts YAML.dump(manifest) } + end diff --git a/artifact/providers/file.rb b/artifact/providers/file.rb new file mode 100644 index 0000000..f2ba02b --- /dev/null +++ b/artifact/providers/file.rb @@ -0,0 +1,105 @@ +# +# Cookbook Name:: artifact +# Provider:: file +# +# Author:: Kyle Allan () +# +# Copyright 2013, Riot Games +# +# 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. +# + +attr_reader :file_location +attr_reader :nexus_configuration +attr_reader :nexus_connection + +def load_current_resource + if Chef::Artifact.from_nexus?(new_resource.location) + chef_gem "nexus_cli" do + version "4.0.2" + end + + @nexus_configuration = new_resource.nexus_configuration + @nexus_connection = Chef::Artifact::Nexus.new(node, nexus_configuration) + end + @file_location = new_resource.location + + @current_resource = Chef::Resource::ArtifactFile.new(@new_resource.name) + @current_resource +end + +action :create do + retries = new_resource.download_retries + begin + if Chef::Artifact.from_s3?(file_location) + unless ::File.exists?(new_resource.name) && checksum_valid? + Chef::Artifact.retrieve_from_s3(node, file_location, new_resource.name) + end + elsif Chef::Artifact.from_nexus?(file_location) + unless ::File.exists?(new_resource.name) && checksum_valid? + begin + nexus_connection.retrieve_from_nexus(file_location, ::File.dirname(new_resource.name)) + rescue NexusCli::PermissionsException => e + msg = "The artifact server returned 401 (Unauthorized) when attempting to retrieve this artifact. Confirm that your credentials are correct." + raise Chef::Artifact::ArtifactDownloadError.new(msg) + end + end + else + remote_file_resource.run_action(:create) + end + raise Chef::Artifact::ArtifactChecksumError unless checksum_valid? + rescue Chef::Artifact::ArtifactChecksumError => e + if retries > 0 + retries -= 1 + Chef::Log.warn "[artifact_file] Downloaded file checksum does not match the provided checksum. Retrying - #{retries} attempt(s) left." + retry + end + raise Chef::Artifact::ArtifactChecksumError + end +end + +# For a Nexus artifact, checks the downloaded file's SHA1 checksum +# against the Nexus Server's entry for the file. For normal HTTP artifact, +# check the passed through checksum or just assume the file is fine. +# +# @return [Boolean] true if the downloaded file's checksum +# matches the checksum on record, false otherwise. +def checksum_valid? + require 'digest' + if Chef::Artifact.from_nexus?(file_location) + Digest::SHA1.file(new_resource.name).hexdigest == nexus_connection.get_artifact_sha(file_location) + else + if new_resource.checksum + Digest::SHA256.file(new_resource.name).hexdigest == new_resource.checksum + else + Chef::Log.debug "[artifact_file] No checksum provided for artifact_file, assuming checksum is valid." + true + end + end +end + +# Creates a remote_file resource that will download the artifact +# and has default idempotency. The action is set to nothing so that +# it can be called later. +# +# @return [Chef::Resource::RemoteFile] +def remote_file_resource + @remote_file_resource ||= remote_file new_resource.name do + source file_location + checksum new_resource.checksum + owner new_resource.owner + group new_resource.group + backup false + action :nothing + end +end diff --git a/artifact/providers/package.rb b/artifact/providers/package.rb new file mode 100644 index 0000000..b510fc3 --- /dev/null +++ b/artifact/providers/package.rb @@ -0,0 +1,69 @@ +# +# Cookbook Name:: artifact +# Provider:: package +# +# Author:: Michael Ivey () +# +# Copyright 2013, Riot Games +# +# 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. +# + +attr_reader :nexus_configuration_object +attr_reader :extension +attr_reader :file_name + +def load_current_resource + if Chef::Artifact.from_nexus?(new_resource.location) + chef_gem "nexus_cli" do + version "4.0.2" + end + require 'nexus_cli' + artifact = NexusCli::Artifact.new(new_resource.location) + @nexus_configuration_object = new_resource.nexus_configuration + @extension = artifact.extension + @file_name = artifact.file_name + else + sha = Digest::SHA1.hexdigest new_resource.location + @extension = new_resource.location.match(/[:\.]([0-9a-z]+)$/i)[1] + @file_name = "#{new_resource.name}-#{sha}.#{ext}" + end + @current_resource = Chef::Resource::ArtifactPackage.new(@new_resource.name) + @current_resource +end + +action :install do + + pkg = ::File.join(Chef::Config[:file_cache_path], + "artifact_packages", + file_name) + + directory ::File.dirname(pkg) do + action :create + recursive true + end + + artifact_file pkg do + location new_resource.location + checksum new_resource.checksum if new_resource.checksum + owner new_resource.owner + group new_resource.group + nexus_configuration nexus_configuration_object if Chef::Artifact.from_nexus?(new_resource.location) + download_retries new_resource.download_retries + end + + package new_resource.name do + source pkg + action :install + end +end diff --git a/artifact/resources/deploy.rb b/artifact/resources/deploy.rb new file mode 100644 index 0000000..38b1ac0 --- /dev/null +++ b/artifact/resources/deploy.rb @@ -0,0 +1,73 @@ +# +# Cookbook Name:: artifact +# Resource:: deploy +# +# Author:: Jamie Winsor () +# Author:: Kyle Allan () +# +# Copyright 2013, Riot Games +# +# 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. +# + +require 'uri' + +actions :deploy, :pre_seed +default_action :deploy + +attribute :artifact_name, :kind_of => String, :required => true, :name_attribute => true +attribute :artifact_location, :kind_of => String +attribute :artifact_checksum, :kind_of => String +attribute :deploy_to, :kind_of => String, :required => true +attribute :version, :kind_of => String, :required => true +attribute :owner, :kind_of => String, :required => true, :regex => Chef::Config[:user_valid_regex] +attribute :group, :kind_of => String, :required => true, :regex => Chef::Config[:user_valid_regex] +attribute :environment, :kind_of => Hash, :default => Hash.new +attribute :symlinks, :kind_of => Hash, :default => Hash.new +attribute :shared_directories, :kind_of => Array, :default => %w{ system pids log } +attribute :force, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :should_migrate, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :keep, :kind_of => Integer, :default => 2 +attribute :is_tarball, :kind_of => [ TrueClass, FalseClass ], :default => true +attribute :before_deploy, :kind_of => Proc +attribute :before_extract, :kind_of => Proc +attribute :after_extract, :kind_of => Proc +attribute :before_symlink, :kind_of => Proc +attribute :after_symlink, :kind_of => Proc +attribute :configure, :kind_of => Proc +attribute :before_migrate, :kind_of => Proc +attribute :after_migrate, :kind_of => Proc +attribute :migrate, :kind_of => Proc +attribute :restart, :kind_of => Proc +attribute :after_deploy, :kind_of => Proc +attribute :remove_top_level_directory, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :skip_manifest_check, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :remove_on_force, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :nexus_configuration, :kind_of => Chef::Artifact::NexusConfiguration, :default => Chef::Artifact::NexusConfiguration.from_data_bag + +def initialize(*args) + super + @action = :deploy +end + +def artifact_deploys_cache_path + ::File.join(Chef::Config[:file_cache_path], "artifact_deploys") +end + +def current_path + ::File.join(self.deploy_to, "current") +end + +def shared_path + ::File.join(self.deploy_to, "shared") +end diff --git a/artifact/resources/file.rb b/artifact/resources/file.rb new file mode 100644 index 0000000..a154877 --- /dev/null +++ b/artifact/resources/file.rb @@ -0,0 +1,31 @@ +# +# Cookbook Name:: artifact +# Resource:: file +# +# Author:: Kyle Allan () +# +# Copyright 2013, Riot Games +# +# 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. +# + +actions :create +default_action :create + +attribute :path, :kind_of => String, :required => true, :name_attribute => true +attribute :location, :kind_of => String +attribute :checksum, :kind_of => String +attribute :owner, :kind_of => String, :required => true, :regex => Chef::Config[:user_valid_regex] +attribute :group, :kind_of => String, :required => true, :regex => Chef::Config[:user_valid_regex] +attribute :download_retries, :kind_of => Integer, :default => 1 +attribute :nexus_configuration, :kind_of => Chef::Artifact::NexusConfiguration, :default => Chef::Artifact::NexusConfiguration.from_data_bag diff --git a/artifact/resources/package.rb b/artifact/resources/package.rb new file mode 100644 index 0000000..96a2502 --- /dev/null +++ b/artifact/resources/package.rb @@ -0,0 +1,31 @@ +# +# Cookbook Name:: artifact +# Resource:: package +# +# Author:: Michael Ivey () +# +# Copyright 2013, Riot Games +# +# 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. +# + +actions :install +default_action :install + +attribute :name, :kind_of => String, :required => true, :name_attribute => true +attribute :location, :kind_of => String +attribute :checksum, :kind_of => String +attribute :owner, :kind_of => String, :regex => Chef::Config[:user_valid_regex], :default => 'root' +attribute :group, :kind_of => String, :regex => Chef::Config[:user_valid_regex], :default => 'root' +attribute :download_retries, :kind_of => Integer, :default => 1 +attribute :nexus_configuration, :kind_of => Chef::Artifact::NexusConfiguration, :default => Chef::Artifact::NexusConfiguration.from_data_bag diff --git a/aws/CHANGELOG.md b/aws/CHANGELOG.md new file mode 100644 index 0000000..0ac450a --- /dev/null +++ b/aws/CHANGELOG.md @@ -0,0 +1,123 @@ +aws Cookbook CHANGELOG +====================== +This file is used to list changes made in each version of the aws cookbook. + +v2.4.0 (2014-08-07) +------------------- +- #64 - force proxy off for metadata queries + +v2.3.0 (2014-07-02) +------------------- +- Added support for provisioning General Purpose (SSD) volumes (gp2) + + +v2.2.2 (2014-05-19) +------------------- +- [COOK-4655] - Require ec2 gem + + +v2.2.0 (2014-04-23) +------------------- +- [COOK-4500] Support IAM roles for ELB + + +v2.1.1 (2014-03-18) +------------------- +- [COOK-4415] disk_existing_raid resource name inconsistency + + +v2.1.0 (2014-02-25) +------------------- +### Improvement +- **[COOK-4008](https://tickets.opscode.com/browse/COOK-4008)** - Add name property for aws_elastic_ip LWRP + + +v2.0.0 (2014-02-19) +------------------- +[COOK-2755] Add allocate action to the elastic ip resource +[COOK-2829] Expose AWS credentials for ebs_raid LWRP as parameters +[COOK-2935] +[COOK-4213] Use use_inline_resources +[COOK-3467] Support IAM role +[COOK-4344] Add support for mounting existing raids and reusing volume +[COOK-3859] Add VPC support (allocation_id) to AWS elastic_ip LWRPJoseph Smith + + +v1.0.0 +------ +### Improvement +- [COOK-2829] -Expose AWS credentials for ebs_raid LWRP as parameters +- Changing attribute defaults begs a major version bump + + +v0.101.6 +-------- +### Bug +- **[COOK-3475](https://tickets.opscode.com/browse/COOK-3475)** - Fix an issuw were invoking action detach in the `ebs_volume `provider when the volume is already detached resulted in a failure + +v0.101.4 +-------- +### Improvement +- **[COOK-3345](https://tickets.opscode.com/browse/COOK-3345)** - Add `aws_s3_file` LWRP +- **[COOK-3264](https://tickets.opscode.com/browse/COOK-3264)** - Allow specifying of file ownership for `ebs_raid` resource `mount_point` + +### Bug +- **[COOK-3308](https://tickets.opscode.com/browse/COOK-3308)** - Ensure mdadm properly allocates the device number + +v0.101.2 +-------- +### Bug + +- [COOK-2951]: aws cookbook has foodcritic failures + +### Improvement + +- [COOK-1471]: aws cookbook should mention the route53 cookbook + +v0.101.0 +-------- +### Bug + +- [COOK-1355]: AWS::ElasticIP recipe uses an old RightAWS API to associate an elastic ip address to an EC2 instance +- [COOK-2659]: `volume_compatible_with_resource_definition` fails on valid `snapshot_id` configurations +- [COOK-2670]: AWS cookbook doesn't use `node[:aws][:databag_name]`, etc. in `create_raid_disks` +- [COOK-2693]: exclude AWS reserved tags from tag update +- [COOK-2914]: Foodcritic failures in Cookbooks + +### Improvement + +- [COOK-2587]: Resource attribute for using most recent snapshot instead of earliest +- [COOK-2605]: "WARN: Missing gem '`right_aws`'" always prints when including 'aws' in metadata + +### New Feature + +- [COOK-2503]: add EBS raid volumes and provisioned IOPS support for AWS + +v0.100.6 +-------- +- [COOK-2148] - `aws_ebs_volume` attach action saves nil `volume_id` in node + +v0.100.4 +-------- +- Support why-run mode in LWRPs +- [COOK-1836] - make `aws_elastic_lb` idempotent + +v0.100.2 +-------- +- [COOK-1568] - switch to chef_gem resource +- [COOK-1426] - declare default actions for LWRPs + +v0.100.0 +-------- +- [COOK-1221] - convert node attribute accessors to strings +- [COOK-1195] - manipulate AWS resource tags (instances, volumes, snapshots +- [COOK-627] - add aws_elb (elastic load balancer) LWRP + +v0.99.1 +------- +- [COOK-530] - aws cookbook doesn't save attributes with chef 0.10.RC.0 +- [COOK-600] - In AWS Cookbook specifying just the device doesn't work +- [COOK-601] - in aws cookbook :prune action keeps 1 less snapshot than snapshots_to_keep +- [COOK-610] - Create Snapshot action in aws cookbook should allow description attribute +- [COOK-819] - fix documentation bug in aws readme +- [COOK-829] - AWS cookbook does not work with most recent right_aws gem but no version is locked in the recipe diff --git a/aws/README.md b/aws/README.md new file mode 100644 index 0000000..1cd49f9 --- /dev/null +++ b/aws/README.md @@ -0,0 +1,434 @@ +Description +=========== + +This cookbook provides libraries, resources and providers to configure +and manage Amazon Web Services components and offerings with the EC2 +API. Currently supported resources: + +* EBS Volumes (`ebs_volume`) +* EBS Raid (`ebs_raid`) +* Elastic IPs (`elastic_ip`) +* Elastic Load Balancer (`elastic_lb`) +* AWS Resource Tags (`resource_tag`) + +Unsupported AWS resources that have other cookbooks include but are +not limited to: + +* [Route53](http://community.opscode.com/cookbooks/route53) + +**Note** This cookbook uses the `right_aws` RubyGem to interact with + the AWS API because at the time it was written, `fog` and `aws-sdk` + were not available. Further, both of those gems require `nokogiri` + which requires compiling native extensions, which means build tools + are required. We do not plan at this time to change the underlying + Ruby library used in order to limit the external dependencies for + this cookbook. + +Requirements +============ + +Requires Chef 0.7.10 or higher for Lightweight Resource and Provider +support. Chef 0.8+ is recommended. While this cookbook can be used in +`chef-solo` mode, to gain the most flexibility, we recommend using +`chef-client` with a Chef Server. + +An Amazon Web Services account is required. The Access Key and Secret +Access Key are used to authenticate with EC2. + +AWS Credentials +=============== + +In order to manage AWS components, authentication credentials need to +be available to the node. There are 2 way to handle this: +1. explicitly pass credentials parameter to the resource +2. or let the resource pick up credentials from the IAM role assigned to the instance + + +## Using resource parameters + +To pass the credentials to the resource, credentials should be available to the node. +There are a number of ways to handle this, such as node attributes or Chef roles. + +We recommend storing these in a databag (Chef 0.8+), and loading them in the recipe where the +resources are needed. + +DataBag recommendation: + + % knife data bag show aws main + { + "id": "main", + "aws_access_key_id": "YOUR_ACCESS_KEY", + "aws_secret_access_key": "YOUR_SECRET_ACCESS_KEY" + } + +This can be loaded in a recipe with: + + aws = data_bag_item("aws", "main") + +And to access the values: + + aws['aws_access_key_id'] + aws['aws_secret_access_key'] + +We'll look at specific usage below. + +## Using IAM instance role + +If your instance has an IAM role, then the credentials can be automatically resolved by the cookbook +using Amazon instance metadata API. + +You can then omit the resource parameters `aws_secret_access_key` and `aws_access_key`. + +Of course, the instance role must have the required policies. Here is a sample policy for EBS volume +management: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ec2:AttachVolume", + "ec2:CreateVolume", + "ec2:ModifyVolumeAttribute", + "ec2:DescribeVolumeAttribute", + "ec2:DescribeVolumeStatus", + "ec2:DescribeVolumes", + "ec2:DetachVolume", + "ec2:EnableVolumeIO" + ], + "Sid": "Stmt1381536011000", + "Resource": [ + "*" + ], + "Effect": "Allow" + } + ] +} +``` + +For resource tags: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ec2:CreateTags" + ], + "Sid": "Stmt1381536708000", + "Resource": [ + "*" + ], + "Effect": "Allow" + } + ] +} +``` + +Recipes +======= + +default.rb +---------- + +The default recipe installs the `right_aws` RubyGem, which this +cookbook requires in order to work with the EC2 API. Make sure that +the aws recipe is in the node or role `run_list` before any resources +from this cookbook are used. + + "run_list": [ + "recipe[aws]" + ] + +The `gem_package` is created as a Ruby Object and thus installed +during the Compile Phase of the Chef run. + +Libraries +========= + +The cookbook has a library module, `Opscode::AWS::Ec2`, which can be +included where necessary: + + include Opscode::Aws::Ec2 + +This is needed in any providers in the cookbook. Along with some +helper methods used in the providers, it sets up a class variable, +`ec2` that is used along with the access and secret access keys + +Resources and Providers +======================= + +This cookbook provides two resources and corresponding providers. + +## ebs_volume.rb + + +Manage Elastic Block Store (EBS) volumes with this resource. + +Actions: + +* `create` - create a new volume. +* `attach` - attach the specified volume. +* `detach` - detach the specified volume. +* `snapshot` - create a snapshot of the volume. +* `prune` - prune snapshots. + +Attribute Parameters: + +* `aws_secret_access_key`, `aws_access_key` - passed to + `Opscode::AWS:Ec2` to authenticate required, unless using IAM roles for authentication. +* `size` - size of the volume in gigabytes. +* `snapshot_id` - snapshot to build EBS volume from. +* most_recent_snapshot - use the most recent snapshot when creating a + volume from an existing volume (defaults to false) +* `availability_zone` - EC2 region, and is normally automatically + detected. +* `device` - local block device to attach the volume to, e.g. + `/dev/sdi` but no default value, required. +* `volume_id` - specify an ID to attach, cannot be used with action + `:create` because AWS assigns new volume IDs +* `timeout` - connection timeout for EC2 API. +* `snapshots_to_keep` - used with action `:prune` for number of + snapshots to maintain. +* `description` - used to set the description of an EBS snapshot +* `volume_type` - "standard" or "io1" (io1 is the type for IOPS volume) +* `piops` - number of Provisioned IOPS to provision, must be >= 100 +* `existing_raid` - whether or not to assume the raid was previously assembled on existing volumes (default no) + +## ebs_raid.rb + +Manage Elastic Block Store (EBS) raid devices with this resource. + +Attribute Parameters: + +* `aws_secret_access_key`, `aws_access_key` - passed to + `Opscode::AWS:Ec2` to authenticate, required. +* `mount_point` - where to mount the RAID volume +* `mount_point_owner` - the owner of the mount point (default root) +* `mount_point_group` - the group of the mount point (default root) +* `mount_point_mode` - the file mode of the mount point (default 0755) +* `disk_count` - number of EBS volumes to raid +* `disk_size` - size of EBS volumes to raid +* `level` - RAID level (default 10) +* `filesystem` - filesystem to format raid array (default ext4) +* `snapshots` - array of EBS snapshots to restore. Snapshots must be + taken using an ec2 consistent snapshot tool, and tagged with a + number that indicates how many devices are in the array being backed + up (e.g. "Logs Backup [0-4]" for a four-volume raid array snapshot) +* `disk_type` - "standard" or "io1" (io1 is the type for IOPS volume) +* `disk_piops` - number of Provisioned IOPS to provision per disk, + must be > 100 + +## elastic_ip.rb + +Actions: + +* `associate` - associate the IP. +* `disassociate` - disassociate the IP. + +Attribute Parameters: + +* `aws_secret_access_key`, `aws_access_key` - passed to + `Opscode::AWS:Ec2` to authenticate, required, unless using IAM roles for authentication. +* `ip` - the IP address. +* `timeout` - connection timeout for EC2 API. + +## elastic_lb.rb + +Actions: + +* `register` - Add this instance to the LB +* `deregister` - Remove this instance from the LB + +Attribute Parameters: + +* `aws_secret_access_key`, `aws_access_key` - passed to + `Opscode::AWS:Ec2` to authenticate, required, unless using IAM roles for authentication. +* `name` - the name of the LB, required. + +## resource_tag.rb + +Actions: + +* `add` - Add tags to a resource. +* `update` - Add or modify existing tags on a resource -- this is the + default action. +* `remove` - Remove tags from a resource, but only if the specified + values match the existing ones. +* `force_remove` - Remove tags from a resource, regardless of their + values. + +Attribute Parameters + +* `aws_secret_access_key`, `aws_access_key` - passed to + `Opscode::AWS:Ec2` to authenticate, required, unless using IAM roles for authentication. +* `tags` - a hash of key value pairs to be used as resource tags, + (e.g. `{ "Name" => "foo", "Environment" => node.chef_environment + }`,) required. +* `resource_id` - resources whose tags will be modified. The value may + be a single ID as a string or multiple IDs in an array. If no + `resource_id` is specified the name attribute will be used. + +Usage +===== + +The following examples assume that the recommended data bag item has +been created and that the following has been included at the top of +the recipe where they are used. + + include_recipe "aws" + aws = data_bag_item("aws", "main") + +## aws_ebs_volume + +The resource only handles manipulating the EBS volume, additional +resources need to be created in the recipe to manage the attached +volume as a filesystem or logical volume. + + aws_ebs_volume "db_ebs_volume" do + aws_access_key aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + size 50 + device "/dev/sdi" + action [ :create, :attach ] + end + +This will create a 50G volume, attach it to the instance as `/dev/sdi`. + + aws_ebs_volume "db_ebs_volume_from_snapshot" do + aws_access_key aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + size 50 + device "/dev/sdi" + snapshot_id "snap-ABCDEFGH" + action [ :create, :attach ] + end + +This will create a new 50G volume from the snapshot ID provided and +attach it as `/dev/sdi`. + +## aws_elastic_ip + +The `elastic_ip` resource provider does not support allocating new +IPs. This must be done before running a recipe that uses the resource. +After allocating a new Elastic IP, we recommend storing it in a +databag and loading the item in the recipe. + +Databag structure: + + % knife data bag show aws eip_load_balancer_production + { + "id": "eip_load_balancer_production", + "public_ip": "YOUR_ALLOCATED_IP" + } + +Then to set up the Elastic IP on a system: + + ip_info = data_bag_item("aws", "eip_load_balancer_production") + + aws_elastic_ip "eip_load_balancer_production" do + aws_access_key aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + ip ip_info['public_ip'] + action :associate + end + +This will use the loaded `aws` and `ip_info` databags to pass the +required values into the resource to configure. Note that when +associating an Elastic IP to an instance, connectivity to the instance +will be lost because the public IP address is changed. You will need +to reconnect to the instance with the new IP. + +You can also store this in a role as an attribute or assign to the +node directly, if preferred. + +## aws_elastic_lb + +`elastic_lb` opererates similar to `elastic_ip'. Make sure that you've +created the ELB and enabled your instances' availability zones prior +to using this provider. + +For example, to register the node in the 'QA' ELB: + + aws_elastic_lb "elb_qa" do + aws_access_key aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + name "QA" + action :register + end + +## aws_resource_tag + +`resource_tag` can be used to manipulate the tags assigned to one or +more AWS resources, i.e. ec2 instances, ebs volumes or ebs volume +snapshots. + +Assigning tags to a node to reflect it's role and environment: + + aws_resource_tag node['ec2']['instance_id'] do + aws_access_key aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + tags({"Name" => "www.example.com app server", + "Environment" => node.chef_environment}) + action :update + end + +Assigning a set of tags to multiple resources, e.g. ebs volumes in a +disk set: + + aws_resource_tag 'my awesome raid set' do + aws_access_key aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + resource_id [ "vol-d0518cb2", "vol-fad31a9a", "vol-fb106a9f", "vol-74ed3b14" ] + tags({"Name" => "My awesome RAID disk set", + "Environment" => node.chef_environment}) + end + +**Note** If you would like to use the normal attribute + `node['aws']['ebs_volume']['db_ebs_volume']['volume_id']` generated by the + aws_ebs_volume resource examples above as input for the resource_id attribute of + the aws_resource_tag resource, you must employ the "lazy" attribute feature + from Chef 10.28 or Chef 11.x and higher. "lazy" will delay the + evaluation of the resource_id attribute's value until the normal/set node attribute is + available. + +``` ruby +aws_resource_tag "db_ebs_volume" do + resource_id lazy { node['aws']['ebs_volume']['db_ebs_volume']['volume_id'] } + tags ({"Service" => "Frontend"}) +end +``` + +## aws_s3_file + +`s3_file` can be used to download a file from s3 that requires aws authorization. This +is a wrapper around `remote_file` and supports the same resource attributes as `remote_file`. + + aws_s3_file "/tmp/foo" do + bucket "i_haz_an_s3_buckit" + remote_path "path/in/s3/bukket/to/foo" + aws_access_key_id aws['aws_access_key_id'] + aws_secret_access_key aws['aws_secret_access_key'] + end + + +License and Author +================== + +* Author:: Chris Walters () +* Author:: AJ Christensen () +* Author:: Justin Huff () + +Copyright 2009-2013, Opscode, Inc. + +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/aws/attributes/default.rb b/aws/attributes/default.rb new file mode 100644 index 0000000..3485e34 --- /dev/null +++ b/aws/attributes/default.rb @@ -0,0 +1,22 @@ +# +# Cookbook Name:: aws +# Attributes:: default +# +# Copyright 2011, Opscode, Inc. +# +# 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. +# + +default['aws']['right_aws_version'] = "3.0.5" +default['aws']['databag_name'] = nil +default['aws']['databag_entry'] = nil diff --git a/aws/libraries/ec2.rb b/aws/libraries/ec2.rb new file mode 100644 index 0000000..ac14e70 --- /dev/null +++ b/aws/libraries/ec2.rb @@ -0,0 +1,102 @@ +# +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# 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. +# + +# TODO: once sync_libraries properly handles sub-directories, move this file to aws/libraries/opscode/aws/ec2.rb + +require 'open-uri' + +module Opscode + module Aws + module Ec2 + def find_snapshot_id(volume_id="", find_most_recent=false) + snapshot_id = nil + snapshots = if find_most_recent + ec2.describe_snapshots.sort { |a,b| a[:aws_started_at] <=> b[:aws_started_at] } + else + ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] } + end + snapshots.each do |snapshot| + if snapshot[:aws_volume_id] == volume_id + snapshot_id = snapshot[:aws_id] + end + end + raise "Cannot find snapshot id!" unless snapshot_id + Chef::Log.debug("Snapshot ID is #{snapshot_id}") + snapshot_id + end + + def ec2 + @@ec2 ||= create_aws_interface(RightAws::Ec2) + end + + def instance_id + @@instance_id ||= query_instance_id + end + + def instance_availability_zone + @@instance_availability_zone ||= query_instance_availability_zone + end + + private + + def create_aws_interface(aws_interface) + begin + require 'right_aws' + rescue LoadError + Chef::Log.error("Missing gem 'right_aws'. Use the default aws recipe to install it first.") + end + + region = instance_availability_zone + region = region[0, region.length-1] + + if new_resource.aws_access_key and new_resource.aws_secret_access_key + aws_interface.new(new_resource.aws_access_key, new_resource.aws_secret_access_key, {:logger => Chef::Log, :region => region}) + else + creds = query_role_credentials + aws_interface.new(creds['AccessKeyId'], creds['SecretAccessKey'], {:logger => Chef::Log, :region => region, :token => creds['Token']}) + end + end + + def query_role + r = open("http://169.254.169.254/latest/meta-data/iam/security-credentials/").readlines.first + r + end + + def query_role_credentials(role = query_role) + fail "Instance has no IAM role." if role.to_s.empty? + creds = open("http://169.254.169.254/latest/meta-data/iam/security-credentials/#{role}",options = {:proxy => false}){|f| JSON.parse(f.string)} + Chef::Log.debug("Retrieved instance credentials for IAM role #{role}") + creds + end + + def query_instance_id + instance_id = open('http://169.254.169.254/latest/meta-data/instance-id',options = {:proxy => false}){|f| f.gets} + raise "Cannot find instance id!" unless instance_id + Chef::Log.debug("Instance ID is #{instance_id}") + instance_id + end + + def query_instance_availability_zone + availability_zone = open('http://169.254.169.254/latest/meta-data/placement/availability-zone/', options = {:proxy => false}){|f| f.gets} + raise "Cannot find availability zone!" unless availability_zone + Chef::Log.debug("Instance's availability zone is #{availability_zone}") + availability_zone + end + + end + end +end diff --git a/aws/libraries/elb.rb b/aws/libraries/elb.rb new file mode 100644 index 0000000..22d9ca4 --- /dev/null +++ b/aws/libraries/elb.rb @@ -0,0 +1,13 @@ +require File.join(File.dirname(__FILE__), 'ec2') + +module Opscode + module Aws + module Elb + include Opscode::Aws::Ec2 + + def elb + @@elb ||= create_aws_interface(RightAws::ElbInterface) + end + end + end +end diff --git a/aws/metadata.json b/aws/metadata.json new file mode 100644 index 0000000..9eb9ef5 --- /dev/null +++ b/aws/metadata.json @@ -0,0 +1,30 @@ +{ + "name": "aws", + "version": "2.4.0", + "description": "LWRPs for managing AWS resources", + "long_description": "Description\n===========\n\nThis cookbook provides libraries, resources and providers to configure\nand manage Amazon Web Services components and offerings with the EC2\nAPI. Currently supported resources:\n\n* EBS Volumes (`ebs_volume`)\n* EBS Raid (`ebs_raid`)\n* Elastic IPs (`elastic_ip`)\n* Elastic Load Balancer (`elastic_lb`)\n* AWS Resource Tags (`resource_tag`)\n\nUnsupported AWS resources that have other cookbooks include but are\nnot limited to:\n\n* [Route53](http://community.opscode.com/cookbooks/route53)\n\n**Note** This cookbook uses the `right_aws` RubyGem to interact with\n the AWS API because at the time it was written, `fog` and `aws-sdk`\n were not available. Further, both of those gems require `nokogiri`\n which requires compiling native extensions, which means build tools\n are required. We do not plan at this time to change the underlying\n Ruby library used in order to limit the external dependencies for\n this cookbook.\n\nRequirements\n============\n\nRequires Chef 0.7.10 or higher for Lightweight Resource and Provider\nsupport. Chef 0.8+ is recommended. While this cookbook can be used in\n`chef-solo` mode, to gain the most flexibility, we recommend using\n`chef-client` with a Chef Server.\n\nAn Amazon Web Services account is required. The Access Key and Secret\nAccess Key are used to authenticate with EC2.\n\nAWS Credentials\n===============\n\nIn order to manage AWS components, authentication credentials need to\nbe available to the node. There are 2 way to handle this:\n1. explicitly pass credentials parameter to the resource\n2. or let the resource pick up credentials from the IAM role assigned to the instance\n\n\n## Using resource parameters\n\nTo pass the credentials to the resource, credentials should be available to the node.\nThere are a number of ways to handle this, such as node attributes or Chef roles.\n\nWe recommend storing these in a databag (Chef 0.8+), and loading them in the recipe where the\nresources are needed.\n\nDataBag recommendation:\n\n % knife data bag show aws main\n {\n \"id\": \"main\",\n \"aws_access_key_id\": \"YOUR_ACCESS_KEY\",\n \"aws_secret_access_key\": \"YOUR_SECRET_ACCESS_KEY\"\n }\n\nThis can be loaded in a recipe with:\n\n aws = data_bag_item(\"aws\", \"main\")\n\nAnd to access the values:\n\n aws['aws_access_key_id']\n aws['aws_secret_access_key']\n\nWe'll look at specific usage below.\n\n## Using IAM instance role\n\nIf your instance has an IAM role, then the credentials can be automatically resolved by the cookbook\nusing Amazon instance metadata API.\n\nYou can then omit the resource parameters `aws_secret_access_key` and `aws_access_key`.\n\nOf course, the instance role must have the required policies. Here is a sample policy for EBS volume\nmanagement:\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": [\n \"ec2:AttachVolume\",\n \"ec2:CreateVolume\",\n \"ec2:ModifyVolumeAttribute\",\n \"ec2:DescribeVolumeAttribute\",\n \"ec2:DescribeVolumeStatus\",\n \"ec2:DescribeVolumes\",\n \"ec2:DetachVolume\",\n \"ec2:EnableVolumeIO\"\n ],\n \"Sid\": \"Stmt1381536011000\",\n \"Resource\": [\n \"*\"\n ],\n \"Effect\": \"Allow\"\n }\n ]\n}\n```\n\nFor resource tags:\n```json\n{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Action\": [\n \"ec2:CreateTags\"\n ],\n \"Sid\": \"Stmt1381536708000\",\n \"Resource\": [\n \"*\"\n ],\n \"Effect\": \"Allow\"\n }\n ]\n}\n```\n\nRecipes\n=======\n\ndefault.rb\n----------\n\nThe default recipe installs the `right_aws` RubyGem, which this\ncookbook requires in order to work with the EC2 API. Make sure that\nthe aws recipe is in the node or role `run_list` before any resources\nfrom this cookbook are used.\n\n \"run_list\": [\n \"recipe[aws]\"\n ]\n\nThe `gem_package` is created as a Ruby Object and thus installed\nduring the Compile Phase of the Chef run.\n\nLibraries\n=========\n\nThe cookbook has a library module, `Opscode::AWS::Ec2`, which can be\nincluded where necessary:\n\n include Opscode::Aws::Ec2\n\nThis is needed in any providers in the cookbook. Along with some\nhelper methods used in the providers, it sets up a class variable,\n`ec2` that is used along with the access and secret access keys\n\nResources and Providers\n=======================\n\nThis cookbook provides two resources and corresponding providers.\n\n## ebs_volume.rb\n\n\nManage Elastic Block Store (EBS) volumes with this resource.\n\nActions:\n\n* `create` - create a new volume.\n* `attach` - attach the specified volume.\n* `detach` - detach the specified volume.\n* `snapshot` - create a snapshot of the volume.\n* `prune` - prune snapshots.\n\nAttribute Parameters:\n\n* `aws_secret_access_key`, `aws_access_key` - passed to\n `Opscode::AWS:Ec2` to authenticate required, unless using IAM roles for authentication.\n* `size` - size of the volume in gigabytes.\n* `snapshot_id` - snapshot to build EBS volume from.\n* most_recent_snapshot - use the most recent snapshot when creating a\n volume from an existing volume (defaults to false)\n* `availability_zone` - EC2 region, and is normally automatically\n detected.\n* `device` - local block device to attach the volume to, e.g.\n `/dev/sdi` but no default value, required.\n* `volume_id` - specify an ID to attach, cannot be used with action\n `:create` because AWS assigns new volume IDs\n* `timeout` - connection timeout for EC2 API.\n* `snapshots_to_keep` - used with action `:prune` for number of\n snapshots to maintain.\n* `description` - used to set the description of an EBS snapshot\n* `volume_type` - \"standard\" or \"io1\" (io1 is the type for IOPS volume)\n* `piops` - number of Provisioned IOPS to provision, must be >= 100\n* `existing_raid` - whether or not to assume the raid was previously assembled on existing volumes (default no)\n\n## ebs_raid.rb\n\nManage Elastic Block Store (EBS) raid devices with this resource.\n\nAttribute Parameters:\n\n* `aws_secret_access_key`, `aws_access_key` - passed to\n `Opscode::AWS:Ec2` to authenticate, required.\n* `mount_point` - where to mount the RAID volume\n* `mount_point_owner` - the owner of the mount point (default root)\n* `mount_point_group` - the group of the mount point (default root)\n* `mount_point_mode` - the file mode of the mount point (default 0755)\n* `disk_count` - number of EBS volumes to raid\n* `disk_size` - size of EBS volumes to raid\n* `level` - RAID level (default 10)\n* `filesystem` - filesystem to format raid array (default ext4)\n* `snapshots` - array of EBS snapshots to restore. Snapshots must be\n taken using an ec2 consistent snapshot tool, and tagged with a\n number that indicates how many devices are in the array being backed\n up (e.g. \"Logs Backup [0-4]\" for a four-volume raid array snapshot)\n* `disk_type` - \"standard\" or \"io1\" (io1 is the type for IOPS volume)\n* `disk_piops` - number of Provisioned IOPS to provision per disk,\n must be > 100\n\n## elastic_ip.rb\n\nActions:\n\n* `associate` - associate the IP.\n* `disassociate` - disassociate the IP.\n\nAttribute Parameters:\n\n* `aws_secret_access_key`, `aws_access_key` - passed to\n `Opscode::AWS:Ec2` to authenticate, required, unless using IAM roles for authentication.\n* `ip` - the IP address.\n* `timeout` - connection timeout for EC2 API.\n\n## elastic_lb.rb\n\nActions:\n\n* `register` - Add this instance to the LB\n* `deregister` - Remove this instance from the LB\n\nAttribute Parameters:\n\n* `aws_secret_access_key`, `aws_access_key` - passed to\n `Opscode::AWS:Ec2` to authenticate, required, unless using IAM roles for authentication.\n* `name` - the name of the LB, required.\n\n## resource_tag.rb\n\nActions:\n\n* `add` - Add tags to a resource.\n* `update` - Add or modify existing tags on a resource -- this is the\n default action.\n* `remove` - Remove tags from a resource, but only if the specified\n values match the existing ones.\n* `force_remove` - Remove tags from a resource, regardless of their\n values.\n\nAttribute Parameters\n\n* `aws_secret_access_key`, `aws_access_key` - passed to\n `Opscode::AWS:Ec2` to authenticate, required, unless using IAM roles for authentication.\n* `tags` - a hash of key value pairs to be used as resource tags,\n (e.g. `{ \"Name\" => \"foo\", \"Environment\" => node.chef_environment\n }`,) required.\n* `resource_id` - resources whose tags will be modified. The value may\n be a single ID as a string or multiple IDs in an array. If no\n `resource_id` is specified the name attribute will be used.\n\nUsage\n=====\n\nThe following examples assume that the recommended data bag item has\nbeen created and that the following has been included at the top of\nthe recipe where they are used.\n\n include_recipe \"aws\"\n aws = data_bag_item(\"aws\", \"main\")\n\n## aws_ebs_volume\n\nThe resource only handles manipulating the EBS volume, additional\nresources need to be created in the recipe to manage the attached\nvolume as a filesystem or logical volume.\n\n aws_ebs_volume \"db_ebs_volume\" do\n aws_access_key aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n size 50\n device \"/dev/sdi\"\n action [ :create, :attach ]\n end\n\nThis will create a 50G volume, attach it to the instance as `/dev/sdi`.\n\n aws_ebs_volume \"db_ebs_volume_from_snapshot\" do\n aws_access_key aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n size 50\n device \"/dev/sdi\"\n snapshot_id \"snap-ABCDEFGH\"\n action [ :create, :attach ]\n end\n\nThis will create a new 50G volume from the snapshot ID provided and\nattach it as `/dev/sdi`.\n\n## aws_elastic_ip\n\nThe `elastic_ip` resource provider does not support allocating new\nIPs. This must be done before running a recipe that uses the resource.\nAfter allocating a new Elastic IP, we recommend storing it in a\ndatabag and loading the item in the recipe.\n\nDatabag structure:\n\n % knife data bag show aws eip_load_balancer_production\n {\n \"id\": \"eip_load_balancer_production\",\n \"public_ip\": \"YOUR_ALLOCATED_IP\"\n }\n\nThen to set up the Elastic IP on a system:\n\n ip_info = data_bag_item(\"aws\", \"eip_load_balancer_production\")\n\n aws_elastic_ip \"eip_load_balancer_production\" do\n aws_access_key aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n ip ip_info['public_ip']\n action :associate\n end\n\nThis will use the loaded `aws` and `ip_info` databags to pass the\nrequired values into the resource to configure. Note that when\nassociating an Elastic IP to an instance, connectivity to the instance\nwill be lost because the public IP address is changed. You will need\nto reconnect to the instance with the new IP.\n\nYou can also store this in a role as an attribute or assign to the\nnode directly, if preferred.\n\n## aws_elastic_lb\n\n`elastic_lb` opererates similar to `elastic_ip'. Make sure that you've\ncreated the ELB and enabled your instances' availability zones prior\nto using this provider.\n\nFor example, to register the node in the 'QA' ELB:\n\n aws_elastic_lb \"elb_qa\" do\n aws_access_key aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n name \"QA\"\n action :register\n end\n\n## aws_resource_tag\n\n`resource_tag` can be used to manipulate the tags assigned to one or\nmore AWS resources, i.e. ec2 instances, ebs volumes or ebs volume\nsnapshots.\n\nAssigning tags to a node to reflect it's role and environment:\n\n aws_resource_tag node['ec2']['instance_id'] do\n aws_access_key aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n tags({\"Name\" => \"www.example.com app server\",\n \"Environment\" => node.chef_environment})\n action :update\n end\n\nAssigning a set of tags to multiple resources, e.g. ebs volumes in a\ndisk set:\n\n aws_resource_tag 'my awesome raid set' do\n aws_access_key aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n resource_id [ \"vol-d0518cb2\", \"vol-fad31a9a\", \"vol-fb106a9f\", \"vol-74ed3b14\" ]\n tags({\"Name\" => \"My awesome RAID disk set\",\n \"Environment\" => node.chef_environment})\n end\n\n**Note** If you would like to use the normal attribute\n `node['aws']['ebs_volume']['db_ebs_volume']['volume_id']` generated by the\n aws_ebs_volume resource examples above as input for the resource_id attribute of\n the aws_resource_tag resource, you must employ the \"lazy\" attribute feature\n from Chef 10.28 or Chef 11.x and higher. \"lazy\" will delay the\n evaluation of the resource_id attribute's value until the normal/set node attribute is\n available.\n\n``` ruby\naws_resource_tag \"db_ebs_volume\" do\n resource_id lazy { node['aws']['ebs_volume']['db_ebs_volume']['volume_id'] }\n tags ({\"Service\" => \"Frontend\"})\nend\n```\n\n## aws_s3_file\n\n`s3_file` can be used to download a file from s3 that requires aws authorization. This\nis a wrapper around `remote_file` and supports the same resource attributes as `remote_file`.\n\n aws_s3_file \"/tmp/foo\" do\n bucket \"i_haz_an_s3_buckit\"\n remote_path \"path/in/s3/bukket/to/foo\"\n aws_access_key_id aws['aws_access_key_id']\n aws_secret_access_key aws['aws_secret_access_key']\n end\n\n\nLicense and Author\n==================\n\n* Author:: Chris Walters ()\n* Author:: AJ Christensen ()\n* Author:: Justin Huff ()\n\nCopyright 2009-2013, Opscode, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n", + "maintainer": "Opscode, Inc.", + "maintainer_email": "cookbooks@opscode.com", + "license": "Apache 2.0", + "platforms": { + }, + "dependencies": { + }, + "recommendations": { + }, + "suggestions": { + }, + "conflicting": { + }, + "providing": { + }, + "replacing": { + }, + "attributes": { + }, + "groupings": { + }, + "recipes": { + "aws": "Installs the right_aws gem during compile time" + } +} \ No newline at end of file diff --git a/aws/providers/ebs_raid.rb b/aws/providers/ebs_raid.rb new file mode 100644 index 0000000..0758c16 --- /dev/null +++ b/aws/providers/ebs_raid.rb @@ -0,0 +1,444 @@ +include Opscode::Aws::Ec2 + +action :auto_attach do + + package "mdadm" do + action :install + end + + # Baseline expectations. + node.set['aws'] ||= {} + node.set[:aws][:raid] ||= {} + + # Mount point information. + node.set[:aws][:raid][@new_resource.mount_point] ||= {} + + # we're done we successfully located what we needed + if !already_mounted(@new_resource.mount_point) && !locate_and_mount(@new_resource.mount_point, @new_resource.mount_point_owner, + @new_resource.mount_point_group, @new_resource.mount_point_mode, + @new_resource.filesystem, @new_resource.filesystem_options) + + # If we get here, we couldn't auto attach, nor re-allocate an existing set of disks to ourselves. Auto create the md devices + + # Stopping udev to ensure RAID md device allocates md0 properly + manage_udev("stop") + + create_raid_disks(@new_resource.mount_point, + @new_resource.mount_point_owner, + @new_resource.mount_point_group, + @new_resource.mount_point_mode, + @new_resource.disk_count, + @new_resource.disk_size, + @new_resource.level, + @new_resource.filesystem, + @new_resource.filesystem_options, + @new_resource.snapshots, + @new_resource.disk_type, + @new_resource.disk_piops, + @new_resource.existing_raid) + + @new_resource.updated_by_last_action(true) + end +end + +private + +# AWS's volume attachment interface assumes that we're using +# sdX style device names. The ones we actually get will be xvdX +def find_free_volume_device_prefix + # Specific to ubuntu 11./12. + vol_dev = "sdh" + + begin + vol_dev = vol_dev.next + base_device = "/dev/#{vol_dev}1" + Chef::Log.info("dev pre trim #{base_device}") + end while ::File.exists?(base_device) + + vol_dev +end + +def find_free_md_device_name + number=0 + #TODO, this won't work with more than 10 md devices + begin + dir = "/dev/md#{number}" + Chef::Log.info("md pre trim #{dir}") + number +=1 + end while ::File.exists?(dir) + + dir[5, dir.length] +end + +def md_device_from_mount_point(mount_point) + md_device = "" + Dir.glob("/dev/md[0-9]*").each do |dir| + # Look at the mount point directory and see if containing device + # is the same as the md device. + if ::File.lstat(dir).rdev == ::File.lstat(mount_point).dev + md_device = dir + break + end + end + md_device +end + +def update_node_from_md_device(md_device, mount_point) + command = "mdadm --misc -D #{md_device} | grep '/dev/s\\|/xv' | awk '{print $7}' | tr '\\n' ' '" + Chef::Log.info("Running #{command}") + raid_devices = `#{command}` + Chef::Log.info("already found the mounted device, created from #{raid_devices}") + + node.set[:aws][:raid][mount_point][:raid_dev] = md_device.sub(/\/dev\//,"") + node.set[:aws][:raid][mount_point][:devices] = raid_devices + node.save unless Chef::Config[:solo] +end + +# Dumb way to look for mounted raid devices. Assumes that the machine +# will only create one. +def find_md_device + md_device = nil + Dir.glob("/dev/md[0-9]*").each do |dir| + Chef::Log.error("More than one /dev/mdX found.") unless md_device.nil? + md_device = dir + end + md_device +end + +def already_mounted(mount_point) + if !::File.exists?(mount_point) + return false + end + + md_device = md_device_from_mount_point(mount_point) + if !md_device || md_device == "" + return false + end + + update_node_from_md_device(md_device, mount_point) + + return true +end + +private +def udev(cmd, log) + execute log do + Chef::Log.debug(log) + command "udevadm control #{cmd}" + end +end + +def update_initramfs() + execute "updating initramfs" do + Chef::Log.debug("updating initramfs to ensure RAID config persists reboots") + command "update-initramfs -u" + end +end + +def manage_udev(action) + if action == "stop" + udev("--stop-exec-queue", "stopping udev...") + elsif action == "start" + udev("--start-exec-queue", "starting udev queued events..") + else + Chef::Log.fatal("Incorrect action passed to manage_udev") + end +end + +# Attempt to find an unused data bag and mount all the EBS volumes to our system +# Note: recovery from this assumed state is weakly untested. +def locate_and_mount(mount_point, mount_point_owner, mount_point_group, mount_point_mode, filesystem, filesystem_options) + + if node['aws'].nil? || node['aws']['raid'].nil? || node['aws']['raid'][mount_point].nil? + Chef::Log.info("No mount point found '#{mount_point}' for node") + return false + end + + if node['aws']['raid'][mount_point]['raid_dev'].nil? || node['aws']['raid'][mount_point]['device_map'].nil? + Chef::Log.info("No raid device found for mount point '#{mount_point}' for node") + return false + end + + raid_dev = node['aws']['raid'][mount_point]['raid_dev'] + devices_string = device_map_to_string(node['aws']['raid'][mount_point]['device_map']) + + Chef::Log.info("Raid device is #{raid_dev} and mount path is #{mount_point}") + + # Stop udev + manage_udev("stop") + + # Mount volumes + mount_volumes(node['aws']['raid'][mount_point]['device_map']) + + # Assemble raid device. + assemble_raid(raid_dev, devices_string) + + # Now mount the drive + mount_device(raid_dev, mount_point, mount_point_owner, mount_point_group, mount_point_mode, filesystem, filesystem_options) + + # update initramfs to ensure RAID config persists reboots + update_initramfs() + + # Start udev back up + manage_udev("start") + + true +end + +# TODO fix this kludge: ideally we'd pull in the device information from the ebs_volume +# resource but it's not up-to-date at this time without breaking this action up. +def correct_device_map(device_map) + corrected_device_map = {} + # Rekey + device_map.keys.each do |k| + if k.start_with?('sd') + new_k = 'xvd' + k[2..-1] + if corrected_device_map.include?(new_k) + Chef::Log.error("Unable to remap due to collision.") + return {} + end + corrected_device_map[new_k] = device_map[k] + else + corrected_device_map[k] = device_map[k] + end + end + corrected_device_map +end + +# Generate the string using the corrected map. +def device_map_to_string(device_map) + corrected_map = correct_device_map(device_map) + + devices_string = "" + corrected_map.keys.sort.each do |k| + devices_string += "/dev/#{k} " + end + devices_string +end + +def mount_volumes(device_vol_map) + # Attach the volumes + device_vol_map.keys.sort.each do |dev_device| + attach_volume(dev_device, device_vol_map[dev_device]) + end + + correct_devices = correct_device_map(device_vol_map).keys.map { |dev| "/dev/#{dev}" } + + # Wait until all volumes are mounted + ruby_block "wait_#{new_resource.name}" do + block do + while not correct_devices.all? { |dev_path| ::File.exists?(dev_path) } + Chef::Log.info("sleeping 3 seconds until EBS volumes have re-attached") + sleep 3 + end + end + end +end + +# Assembles the raid if it doesn't already exist +# Note: raid_dev is the "suggested" location. mdadm may actually put it somewhere else. +def assemble_raid(raid_dev, devices_string) + if ::File.exists?(raid_dev) + Chef::Log.info("Device #{raid_dev} exists skipping") + return + end + + Chef::Log.info("Raid device #{raid_dev} does not exist re-assembling") + Chef::Log.debug("Devices for #{raid_dev} are #{devices_string}") + + # Now that attach is done we re-build the md device + # We have to grab the UUID of the md device or the disks will be assembled with the UUID stored + # within the superblock metadata, causing the md_device number to be randomly + # chosen if restore is happening on a different host + execute "re-attaching raid device" do + command "mdadm -A --uuid=`mdadm -E --scan|awk '{print $4}'|sed 's/UUID=//g'` #{raid_dev} #{devices_string}" + # mdadm may return 2 but still return a clean raid device. + returns [0, 2] + end +end + +def mount_device(raid_dev, mount_point, mount_point_owner, mount_point_group, mount_point_mode, filesystem, filesystem_options) + # Create the mount point + directory mount_point do + owner mount_point_owner + group mount_point_group + mode mount_point_mode + action :create + not_if "test -d #{mount_point}" + end + + # Try to figure out the actual device. + ruby_block "find md device in #{new_resource.name}" do + block do + if ::File.exists?(mount_point) + Chef::Log.info("Already mounted: #{mount_point}") + end + + # For some silly reason we can't call the function. + md_device = nil + Dir.glob("/dev/md[0-9]*").each do |dir| + Chef::Log.error("More than one /dev/mdX found.") unless md_device.nil? + md_device = dir + end + + Chef::Log.info("Found #{md_device}") + + # the mountpoint must be determined dynamically, so I can't use the chef mount + system("mount -t #{filesystem} -o #{filesystem_options} #{md_device} #{mount_point}") + end + end +end + +# Attach all existing ami instances if they exist on this node, if not, we want an error to occur Detects disk from node information +def attach_volume(disk_dev, volume_id) + disk_dev_path = "/dev/#{disk_dev}" + + Chef::Log.info("Attaching existing ebs volume id #{volume_id} for device #{disk_dev_path}") + + creds = aws_creds() # cannot be invoked inside the block + aws_ebs_volume disk_dev_path do + aws_access_key creds['aws_access_key_id'] + aws_secret_access_key creds['aws_secret_access_key'] + device disk_dev_path + name disk_dev + volume_id volume_id + action [:attach] + provider "aws_ebs_volume" + end +end + +# Mount point for where to mount I.E /mnt/filesystem +# Diskset I.E sdi (which creates sdi1-sdi +# Raid size. The total size of the array +# Raid level. The raid level to use. +# Filesystem. The file system to create. +# Filesystem_options The options to pass to mount +# Snapshots. The list of snapshots to create the ebs volumes from. +# If it's not nil, must have exactly elements + +def create_raid_disks(mount_point, mount_point_owner, mount_point_group, mount_point_mode, num_disks, disk_size, + level, filesystem, filesystem_options, snapshots, disk_type, disk_piops, existing_raid ) + + creating_from_snapshot = !(snapshots.nil? || snapshots.size == 0) + + disk_dev = find_free_volume_device_prefix + Chef::Log.debug("vol device prefix is #{disk_dev}") + + raid_dev = find_free_md_device_name + Chef::Log.debug("target raid device is #{raid_dev}") + + devices = {} + + # For each volume add information to the mount metadata + (1..num_disks).each do |i| + + disk_dev_path = "#{disk_dev}#{i}" + + Chef::Log.info "Snapshot array is #{snapshots[i-1]}" + creds = aws_creds() # cannot be invoked inside the block + aws_ebs_volume disk_dev_path do + aws_access_key creds['aws_access_key_id'] + aws_secret_access_key creds['aws_secret_access_key'] + size disk_size + volume_type disk_type + piops disk_piops + device "/dev/#{disk_dev_path}" + name disk_dev_path + action [:create, :attach] + snapshot_id creating_from_snapshot ? snapshots[i-1] : "" + provider "aws_ebs_volume" + + # set up our data bag info + devices[disk_dev_path] = "pending" + + Chef::Log.info("creating ebs volume for device #{disk_dev_path} with size #{disk_size}") + end + + Chef::Log.info("attach dev: #{disk_dev_path}") + end + + ruby_block "sleeping_#{new_resource.name}" do + block do + Chef::Log.debug("sleeping 10 seconds to let drives attach") + sleep 10 + end + end + + # Create the raid device strings w/sd => xvd correction + devices_string = device_map_to_string(devices) + Chef::Log.info("finished sorting devices #{devices_string}") + + if not creating_from_snapshot and not existing_raid + # Create the raid device on our system + execute "creating raid device" do + Chef::Log.info("creating raid device /dev/#{raid_dev} with raid devices #{devices_string}") + command "[ -b /dev/#{raid_dev} ] && mdadm --stop /dev/#{raid_dev} ; yes | mdadm --create /dev/#{raid_dev} --level=#{level} --raid-devices=#{devices.size} #{devices_string}" + end + + # NOTE: must be a better way. + # Try to figure out the actual device. + ruby_block "formatting md device in #{new_resource.name}" do + block do + # For some silly reason we can't call the function. + md_device = nil + Dir.glob("/dev/md[0-9]*").each do |dir| + Chef::Log.error("More than one /dev/mdX found.") unless md_device.nil? + md_device = dir + end + + Chef::Log.info("Format device found: #{md_device}") + case filesystem + when "ext4" + system("mke2fs -t #{filesystem} -F #{md_device}") + else + #TODO fill in details on how to format other filesystems here + Chef::Log.info("Can't format filesystem #{filesystem}") + end + end + end + else + # Reassembling the raid device on our system + assemble_raid("/dev/#{raid_dev}", devices_string) + end + + # start udev + manage_udev("start") + + # Mount the device + mount_device(raid_dev, mount_point, mount_point_owner, mount_point_group, mount_point_mode, filesystem, filesystem_options) + + # update initramfs to ensure RAID config persists reboots + update_initramfs() + + # Not invoked until the volumes have been successfully created and attached + ruby_block "databagupdate" do + block do + Chef::Log.info("finished creating disks") + + devices.each_pair do |key, value| + value = node['aws']['ebs_volume'][key]['volume_id'] + devices[key] = value + Chef::Log.info("value is #{value}") + end + + # Assemble all the data bag meta data + node.set[:aws][:raid][mount_point][:raid_dev] = raid_dev + node.set[:aws][:raid][mount_point][:device_map] = devices + node.save unless Chef::Config[:solo] + end + end + +end + +def aws_creds + h = {} + if new_resource.aws_access_key && new_resource.aws_secret_access_key + h['aws_access_key_id'] = new_resource.aws_access_key + h['aws_secret_access_key'] = new_resource.aws_secret_access_key + elsif node['aws']['databag_name'] && node['aws']['databag_entry'] + Chef::Log.warn("DEPRECATED: node['aws']['databag_name'] and node['aws']['databag_entry'] are deprecated. Use LWRP parameters instead.") + h = data_bag_item(node['aws']['databag_name'], node['aws']['databag_entry']) + end + h +end + diff --git a/aws/providers/ebs_volume.rb b/aws/providers/ebs_volume.rb new file mode 100644 index 0000000..926ccfe --- /dev/null +++ b/aws/providers/ebs_volume.rb @@ -0,0 +1,267 @@ +include Opscode::Aws::Ec2 + +# Support whyrun +def whyrun_supported? + true +end + +action :create do + raise "Cannot create a volume with a specific id (EC2 chooses volume ids)" if new_resource.volume_id + if new_resource.snapshot_id =~ /vol/ + new_resource.snapshot_id(find_snapshot_id(new_resource.snapshot_id, new_resource.most_recent_snapshot)) + end + + nvid = volume_id_in_node_data + if nvid + # volume id is registered in the node data, so check that the volume in fact exists in EC2 + vol = volume_by_id(nvid) + exists = vol && vol[:aws_status] != "deleting" + # TODO: determine whether this should be an error or just cause a new volume to be created. Currently erring on the side of failing loudly + raise "Volume with id #{nvid} is registered with the node but does not exist in EC2. To clear this error, remove the ['aws']['ebs_volume']['#{new_resource.name}']['volume_id'] entry from this node's data." unless exists + else + # Determine if there is a volume that meets the resource's specifications and is attached to the current + # instance in case a previous [:create, :attach] run created and attached a volume but for some reason was + # not registered in the node data (e.g. an exception is thrown after the attach_volume request was accepted + # by EC2, causing the node data to not be stored on the server) + if new_resource.device && (attached_volume = currently_attached_volume(instance_id, new_resource.device)) + Chef::Log.debug("There is already a volume attached at device #{new_resource.device}") + compatible = volume_compatible_with_resource_definition?(attached_volume) + raise "Volume #{attached_volume[:aws_id]} attached at #{attached_volume[:aws_device]} but does not conform to this resource's specifications" unless compatible + Chef::Log.debug("The volume matches the resource's definition, so the volume is assumed to be already created") + converge_by("update the node data with volume id: #{attached_volume[:aws_id]}") do + node.set['aws']['ebs_volume'][new_resource.name]['volume_id'] = attached_volume[:aws_id] + node.save unless Chef::Config[:solo] + end + else + # If not, create volume and register its id in the node data + converge_by("create a volume with id=#{new_resource.snapshot_id} size=#{new_resource.size} availability_zone=#{new_resource.availability_zone} and update the node data with created volume's id") do + nvid = create_volume(new_resource.snapshot_id, + new_resource.size, + new_resource.availability_zone, + new_resource.timeout, + new_resource.volume_type, + new_resource.piops) + node.set['aws']['ebs_volume'][new_resource.name]['volume_id'] = nvid + node.save unless Chef::Config[:solo] + end + end + end +end + +action :attach do + # determine_volume returns a Hash, not a Mash, and the keys are + # symbols, not strings. + vol = determine_volume + + if vol[:aws_status] == "in-use" + if vol[:aws_instance_id] != instance_id + raise "Volume with id #{vol[:aws_id]} exists but is attached to instance #{vol[:aws_instance_id]}" + else + Chef::Log.debug("Volume is already attached") + end + else + converge_by("attach the volume with aws_id=#{vol[:aws_id]} id=#{instance_id} device=#{new_resource.device} and update the node data with created volume's id") do + # attach the volume and register its id in the node data + attach_volume(vol[:aws_id], instance_id, new_resource.device, new_resource.timeout) + # always use a symbol here, it is a Hash + node.set['aws']['ebs_volume'][new_resource.name]['volume_id'] = vol[:aws_id] + node.save unless Chef::Config[:solo] + end + end +end + +action :detach do + vol = determine_volume + converge_by("detach volume with id: #{vol[:aws_id]}") do + detach_volume(vol[:aws_id], new_resource.timeout) + end +end + +action :snapshot do + vol = determine_volume + converge_by("would create a snapshot for volume: #{vol[:aws_id]}") do + snapshot = ec2.create_snapshot(vol[:aws_id],new_resource.description) + Chef::Log.info("Created snapshot of #{vol[:aws_id]} as #{snapshot[:aws_id]}") + end +end + +action :prune do + vol = determine_volume + old_snapshots = Array.new + Chef::Log.info "Checking for old snapshots" + ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot| + if snapshot[:aws_volume_id] == vol[:aws_id] + Chef::Log.info "Found old snapshot #{snapshot[:aws_id]} (#{snapshot[:aws_volume_id]}) #{snapshot[:aws_started_at]}" + old_snapshots << snapshot + end + end + if old_snapshots.length > new_resource.snapshots_to_keep + old_snapshots[new_resource.snapshots_to_keep, old_snapshots.length].each do |die| + converge_by("delete snapshot with id: #{die[:aws_id]}") do + Chef::Log.info "Deleting old snapshot #{die[:aws_id]}" + ec2.delete_snapshot(die[:aws_id]) + end + end + end +end + +private + +def volume_id_in_node_data + begin + node['aws']['ebs_volume'][new_resource.name]['volume_id'] + rescue NoMethodError => e + nil + end +end + +# Pulls the volume id from the volume_id attribute or the node data and verifies that the volume actually exists +def determine_volume + vol = currently_attached_volume(instance_id, new_resource.device) + vol_id = new_resource.volume_id || volume_id_in_node_data || ( vol ? vol[:aws_id] : nil ) + raise "volume_id attribute not set and no volume id is set in the node data for this resource (which is populated by action :create) and no volume is attached at the device" unless vol_id + + # check that volume exists + vol = volume_by_id(vol_id) + raise "No volume with id #{vol_id} exists" unless vol + + vol +end + +# Retrieves information for a volume +def volume_by_id(volume_id) + ec2.describe_volumes.find{|v| v[:aws_id] == volume_id} +end + +# Returns the volume that's attached to the instance at the given device or nil if none matches +def currently_attached_volume(instance_id, device) + ec2.describe_volumes.find{|v| v[:aws_instance_id] == instance_id && v[:aws_device] == device} +end + +# Returns true if the given volume meets the resource's attributes +def volume_compatible_with_resource_definition?(volume) + if new_resource.snapshot_id =~ /vol/ + new_resource.snapshot_id(find_snapshot_id(new_resource.snapshot_id, new_resource.most_recent_snapshot)) + end + (new_resource.size.nil? || new_resource.size == volume[:aws_size]) && + (new_resource.availability_zone.nil? || new_resource.availability_zone == volume[:zone]) && + (new_resource.snapshot_id.nil? || new_resource.snapshot_id == volume[:snapshot_id]) +end + +# Creates a volume according to specifications and blocks until done (or times out) +def create_volume(snapshot_id, size, availability_zone, timeout, volume_type, piops) + availability_zone ||= instance_availability_zone + + # Sanity checks so we don't shoot ourselves. + raise "Invalid volume type: #{volume_type}" unless ['standard', 'io1', 'gp2'].include?(volume_type) + + # PIOPs requested. Must specify an iops param and probably won't be "low". + if volume_type == 'io1' + raise 'IOPS value not specified.' unless piops >= 100 + end + + # Shouldn't see non-zero piops param without appropriate type. + if piops > 0 + raise 'IOPS param without piops volume type.' unless volume_type == 'io1' + end + + create_volume_opts = { :volume_type => volume_type } + # TODO: this may have to be casted to a string. rightaws vs aws doc discrepancy. + create_volume_opts[:iops] = piops if volume_type == 'io1' + + nv = ec2.create_volume(snapshot_id, size, availability_zone, create_volume_opts) + Chef::Log.debug("Created new volume #{nv[:aws_id]}#{snapshot_id ? " based on #{snapshot_id}" : ""}") + + # block until created + begin + Timeout::timeout(timeout) do + while true + vol = volume_by_id(nv[:aws_id]) + if vol && vol[:aws_status] != "deleting" + if ["in-use", "available"].include?(vol[:aws_status]) + Chef::Log.info("Volume #{nv[:aws_id]} is available") + break + else + Chef::Log.debug("Volume is #{vol[:aws_status]}") + end + sleep 3 + else + raise "Volume #{nv[:aws_id]} no longer exists" + end + end + end + rescue Timeout::Error + raise "Timed out waiting for volume creation after #{timeout} seconds" + end + + nv[:aws_id] +end + +# Attaches the volume and blocks until done (or times out) +def attach_volume(volume_id, instance_id, device, timeout) + Chef::Log.debug("Attaching #{volume_id} as #{device}") + ec2.attach_volume(volume_id, instance_id, device) + + # block until attached + begin + Timeout::timeout(timeout) do + while true + vol = volume_by_id(volume_id) + if vol && vol[:aws_status] != "deleting" + if vol[:aws_attachment_status] == "attached" + if vol[:aws_instance_id] == instance_id + Chef::Log.info("Volume #{volume_id} is attached to #{instance_id}") + break + else + raise "Volume is attached to instance #{vol[:aws_instance_id]} instead of #{instance_id}" + end + else + Chef::Log.debug("Volume is #{vol[:aws_status]}") + end + sleep 3 + else + raise "Volume #{volume_id} no longer exists" + end + end + end + rescue Timeout::Error + raise "Timed out waiting for volume attachment after #{timeout} seconds" + end +end + +# Detaches the volume and blocks until done (or times out) +def detach_volume(volume_id, timeout) + vol = volume_by_id(volume_id) + if vol[:aws_instance_id] != instance_id + Chef::Log.debug("EBS Volume #{volume_id} is not attached to this instance (attached to #{vol[:aws_instance_id]}). Skipping...") + return + end + Chef::Log.debug("Detaching #{volume_id}") + orig_instance_id = vol[:aws_instance_id] + ec2.detach_volume(volume_id) + + # block until detached + begin + Timeout::timeout(timeout) do + while true + vol = volume_by_id(volume_id) + if vol && vol[:aws_status] != "deleting" + if vol[:aws_instance_id] != orig_instance_id + Chef::Log.info("Volume detached from #{orig_instance_id}") + break + else + Chef::Log.debug("Volume: #{vol.inspect}") + end + else + Chef::Log.debug("Volume #{volume_id} no longer exists") + break + end + sleep 3 + end + end + rescue Timeout::Error + raise "Timed out waiting for volume detachment after #{timeout} seconds" + end +end + + diff --git a/aws/providers/elastic_ip.rb b/aws/providers/elastic_ip.rb new file mode 100644 index 0000000..2793f01 --- /dev/null +++ b/aws/providers/elastic_ip.rb @@ -0,0 +1,113 @@ +include Opscode::Aws::Ec2 + +# Support whyrun +def whyrun_supported? + true +end + +action :associate do + ip = new_resource.ip || node['aws']['elastic_ip'][new_resource.name]['ip'] + addr = address(ip) + + if addr.nil? + raise "Elastic IP #{ip} does not exist" + elsif addr[:instance_id] == instance_id + Chef::Log.debug("Elastic IP #{ip} is already attached to the instance") + else + converge_by("attach Elastic IP #{ip} to the instance") do + Chef::Log.info("Attaching Elastic IP #{ip} to the instance") + attach(ip, new_resource.timeout) + node.set['aws']['elastic_ip'][new_resource.name]['ip'] = ip + node.save unless Chef::Config[:solo] + end + end +end + +action :disassociate do + ip = new_resource.ip || node['aws']['elastic_ip'][new_resource.name]['ip'] + addr = address(ip) + + if addr.nil? + Chef::Log.debug("Elastic IP #{ip} does not exist, so there is nothing to detach") + elsif addr[:instance_id] != instance_id + Chef::Log.debug("Elastic IP #{ip} is already detached from the instance") + else + converge_by("detach Elastic IP #{ip} from the instance") do + Chef::Log.info("Detaching Elastic IP #{ip} from the instance") + detach(ip, new_resource.timeout) + end + end +end + +action :allocate do + current_elastic_ip = node['aws']['elastic_ip'][new_resource.name]['ip'] + if current_elastic_ip + Chef::Log.info("An Elastic IP was already allocated for #{new_resource.name} #{current_elastic_ip} from the instance") + else + converge_by("allocate new Elastic IP for #{new_resource.name}") do + addr = ec2.allocate_address + Chef::Log.info("Allocated Elastic IP #{addr[:public_ip]} from the instance") + node.set['aws']['elastic_ip'][new_resource.name]['ip'] = addr[:public_ip] + node.save unless Chef::Config[:solo] + end + end +end + +private + +def address(ip) + ec2.describe_addresses.find { |a| a[:public_ip] == ip } +end + +def attach(ip, timeout) + addr = address(ip) + if addr[:domain] == 'vpc' + ec2.associate_address(instance_id, {:allocation_id => addr[:allocation_id]}) + else + ec2.associate_address(instance_id, {:public_ip => addr[:public_ip]}) + end + + # block until attached + begin + Timeout::timeout(timeout) do + while true + addr = address(ip) + if addr.nil? + raise "Elastic IP has been deleted while waiting for attachment" + elsif addr[:instance_id] == instance_id + Chef::Log.debug("Elastic IP is attached to this instance") + break + else + Chef::Log.debug("Elastic IP is currently attached to #{addr[:instance_id]}") + end + sleep 3 + end + end + rescue Timeout::Error + raise "Timed out waiting for attachment after #{timeout} seconds" + end +end + +def detach(ip, timeout) + ec2.disassociate_address({:public_ip => ip}) + + # block until detached + begin + Timeout::timeout(timeout) do + while true + addr = address(ip) + if addr.nil? + Chef::Log.debug("Elastic IP has been deleted while waiting for detachment") + elsif addr[:instance_id] != instance_id + Chef::Log.debug("Elastic IP is detached from this instance") + break + else + Chef::Log.debug("Elastic IP is still attached") + end + sleep 3 + end + end + rescue Timeout::Error + raise "Timed out waiting for detachment after #{timeout} seconds" + end +end diff --git a/aws/providers/elastic_lb.rb b/aws/providers/elastic_lb.rb new file mode 100644 index 0000000..9522323 --- /dev/null +++ b/aws/providers/elastic_lb.rb @@ -0,0 +1,25 @@ +include Opscode::Aws::Elb + +action :register do + converge_by("add the node #{new_resource.name} to ELB") do + target_lb = elb.describe_load_balancers.find {|lb| lb[:load_balancer_name] == new_resource.name } + unless target_lb[:instances].include?(instance_id) + Chef::Log.info("Adding node to ELB #{new_resource.name}") + elb.register_instances_with_load_balancer(new_resource.name, instance_id) + else + Chef::Log.debug("Node #{instance_id} is already present in ELB instances, no action required.") + end + end +end + +action :deregister do + converge_by("remove the node #{new_resource.name} from ELB") do + target_lb = elb.describe_load_balancers.find {|lb| lb[:load_balancer_name] == new_resource.name } + if target_lb[:instances].include?(instance_id) + Chef::Log.info("Removing node from ELB #{new_resource.name}") + elb.deregister_instances_with_load_balancer(new_resource.name, instance_id) + else + Chef::Log.debug("Node #{instance_id} is not present in ELB instances, no action required.") + end + end +end diff --git a/aws/providers/resource_tag.rb b/aws/providers/resource_tag.rb new file mode 100644 index 0000000..5ff94be --- /dev/null +++ b/aws/providers/resource_tag.rb @@ -0,0 +1,95 @@ +include Opscode::Aws::Ec2 + +action :add do + + unless @new_resource.resource_id + resource_id = @new_resource.name + else + resource_id = @new_resource.resource_id + end + + @new_resource.tags.each do |k,v| + unless @current_resource.tags.keys.include?(k) + converge_by("add tag '#{k}' with value '#{v}' on resource #{resource_id}") do + ec2.create_tags(resource_id, { k => v }) + Chef::Log.info("AWS: Added tag '#{k}' with value '#{v}' on resource #{resource_id}") + end + else + Chef::Log.debug("AWS: Resource #{resource_id} already has a tag with key '#{k}', will not add tag '#{k}' => '#{v}'") + end + end +end + +action :update do + unless @new_resource.resource_id + resource_id = @new_resource.name + else + resource_id = @new_resource.resource_id + end + + updated_tags = @current_resource.tags.merge(@new_resource.tags) + unless updated_tags.eql?(@current_resource.tags) + # tags that begin with "aws" are reserved + converge_by("Updating the following tags for resource #{resource_id} (skipping AWS tags): " + updated_tags.inspect) do + Chef::Log.info("AWS: Updating the following tags for resource #{resource_id} (skipping AWS tags): " + updated_tags.inspect) + updated_tags.delete_if { |key, value| key.to_s.match /^aws/ } + ec2.create_tags(resource_id, updated_tags) + end + else + Chef::Log.debug("AWS: Tags for resource #{resource_id} are unchanged") + end +end + +action :remove do + unless @new_resource.resource_id + resource_id = @new_resource.name + else + resource_id = @new_resource.resource_id + end + + tags_to_delete = @new_resource.tags.keys + + tags_to_delete.each do |key| + if @current_resource.tags.keys.include?(key) and @current_resource.tags[key] == @new_resource.tags[key] + converge_by("delete tag '#{key}' on resource #{resource_id} with value '#{@current_resource.tags[key]}'") do + ec2.delete_tags(resource_id, {key => @new_resource.tags[key]}) + Chef::Log.info("AWS: Deleted tag '#{key}' on resource #{resource_id} with value '#{@current_resource.tags[key]}'") + end + end + end +end + +action :force_remove do + unless @new_resource.resource_id + resource_id = @new_resource.name + else + resource_id = @new_resource.resource_id + end + + @new_resource.tags.keys do |key| + if @current_resource.tags.keys.include?(key) + converge_by("AWS: Deleted tag '#{key}' on resource #{resource_id} with value '#{@current_resource.tags[key]}'") do + ec2.delete_tags(resource_id, key) + Chef::Log.info("AWS: Deleted tag '#{key}' on resource #{resource_id} with value '#{@current_resource.tags[key]}'") + end + end + end +end + +def load_current_resource + @current_resource = Chef::Resource::AwsResourceTag.new(@new_resource.name) + @current_resource.name(@new_resource.name) + unless @new_resource.resource_id + @current_resource.resource_id(@new_resource.name) + else + @current_resource.resource_id(@new_resource.resource_id) + end + + @current_resource.tags(Hash.new) + + ec2.describe_tags(:filters => { 'resource-id' => @current_resource.resource_id }).map { + |tag| @current_resource.tags[tag[:key]] = tag[:value] + } + + @current_resource +end diff --git a/aws/providers/s3_file.rb b/aws/providers/s3_file.rb new file mode 100644 index 0000000..432c6ca --- /dev/null +++ b/aws/providers/s3_file.rb @@ -0,0 +1,59 @@ + +use_inline_resources if defined?(use_inline_resources) + +def whyrun_supported? + true +end + +action :create do + do_s3_file(:create) +end + +action :create_if_missing do + do_s3_file(:create_if_missing) +end + +action :delete do + do_s3_file(:delete) +end + +action :touch do + do_s3_file(:touch) +end + + +def do_s3_file(resource_action) + version = Chef::Version.new(Chef::VERSION[/^(\d+\.\d+\.\d+)/, 1]) + if version.major < 11 || (version.major == 11 && version.minor < 6) + Chef::Log.warn("In order to automatically use etag support to prevent re-downloading files from s3, you must upgrade to at least chef 11.6.0") + end + + remote_path = new_resource.remote_path + remote_path.sub!(/^\/*/, "") + + s3url = RightAws::S3Interface.new(new_resource.aws_access_key_id, new_resource.aws_secret_access_key).get_link(new_resource.bucket, remote_path) + + remote_file new_resource.name do + path new_resource.path + source s3url + owner new_resource.owner + group new_resource.group + mode new_resource.mode + checksum new_resource.checksum + backup new_resource.backup + if node['platform_family'] == "windows" + inherits new_resource.inherits + rights new_resource.rights + end + action resource_action + + if version.major > 11 || (version.major == 11 && version.minor >= 6) + headers new_resource.headers + use_etag new_resource.use_etag + use_last_modified new_resource.use_last_modified + atomic_update new_resource.atomic_update + force_unlink new_resource.force_unlink + manage_symlink_source new_resource.manage_symlink_source + end + end +end diff --git a/aws/recipes/default.rb b/aws/recipes/default.rb new file mode 100644 index 0000000..99f1ffb --- /dev/null +++ b/aws/recipes/default.rb @@ -0,0 +1,25 @@ +# +# Cookbook Name:: aws +# Recipe:: default +# +# Copyright 2008-2009, Opscode, Inc. +# +# 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. +# + +chef_gem "right_aws" do + version node['aws']['right_aws_version'] + action :install +end + +require 'right_aws' diff --git a/aws/resources/ebs_raid.rb b/aws/resources/ebs_raid.rb new file mode 100644 index 0000000..eed0b6f --- /dev/null +++ b/aws/resources/ebs_raid.rb @@ -0,0 +1,35 @@ +actions :auto_attach + +default_action :auto_attach + +state_attrs :aws_access_key, + :disk_count, + :disk_piops, + :disk_size, + :disk_type, + :filesystem, + :filesystem_options, + :level, + :mount_point, + :mount_point_group, + :mount_point_mode, + :mount_point_owner, + :snapshots + + +attribute :aws_access_key, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :mount_point, :kind_of => String +attribute :mount_point_owner, :kind_of => String, :default => 'root' +attribute :mount_point_group, :kind_of => String, :default => 'root' +attribute :mount_point_mode, :kind_of => String, :default => 00755 +attribute :disk_count, :kind_of => Integer +attribute :disk_size, :kind_of => Integer +attribute :level, :default => 10 +attribute :filesystem, :default => "ext4" +attribute :filesystem_options, :default => "rw,noatime,nobootwait" +attribute :snapshots, :default => [] +attribute :disk_type, :kind_of => String, :default => 'standard' +attribute :disk_piops, :kind_of => Integer, :default => 0 +attribute :existing_raid, :kind_of => [ TrueClass, FalseClass ] + diff --git a/aws/resources/ebs_volume.rb b/aws/resources/ebs_volume.rb new file mode 100644 index 0000000..b6aaf30 --- /dev/null +++ b/aws/resources/ebs_volume.rb @@ -0,0 +1,33 @@ +actions :create, :attach, :detach, :snapshot, :prune + +state_attrs :availability_zone, + :aws_access_key, + :description, + :device, + :most_recent_snapshot, + :piops, + :size, + :snapshot_id, + :snapshots_to_keep, + :timeout, + :volume_id, + :volume_type + +attribute :aws_access_key, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :size, :kind_of => Integer +attribute :snapshot_id, :kind_of => String +attribute :most_recent_snapshot, :kind_of => [TrueClass, FalseClass], :default => false +attribute :availability_zone, :kind_of => String +attribute :device, :kind_of => String +attribute :volume_id, :kind_of => String +attribute :description, :kind_of => String +attribute :timeout, :default => 3*60 # 3 mins, nil or 0 for no timeout +attribute :snapshots_to_keep, :default => 2 +attribute :volume_type, :kind_of => String, :default => 'standard' +attribute :piops, :kind_of => Integer, :default => 0 + +def initialize(*args) + super + @action = :create +end diff --git a/aws/resources/elastic_ip.rb b/aws/resources/elastic_ip.rb new file mode 100644 index 0000000..d3d723c --- /dev/null +++ b/aws/resources/elastic_ip.rb @@ -0,0 +1,15 @@ +actions :associate, :disassociate, :allocate + +state_attrs :aws_access_key, + :ip, + :timeout + +attribute :aws_access_key, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :ip, :kind_of => String, :name_attribute => true +attribute :timeout, :default => 3*60 # 3 mins, nil or 0 for no timeout + +def initialize(*args) + super + @action = :associate +end diff --git a/aws/resources/elastic_lb.rb b/aws/resources/elastic_lb.rb new file mode 100644 index 0000000..54ea584 --- /dev/null +++ b/aws/resources/elastic_lb.rb @@ -0,0 +1,13 @@ +actions :register, :deregister + +state_attrs :aws_access_key, + :name + +attribute :aws_access_key, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :name, :kind_of => String + +def initialize(*args) + super + @action = :register +end diff --git a/aws/resources/resource_tag.rb b/aws/resources/resource_tag.rb new file mode 100644 index 0000000..cd8bed7 --- /dev/null +++ b/aws/resources/resource_tag.rb @@ -0,0 +1,15 @@ +def initialize(*args) + super + @action = :update +end + +actions :add, :update, :remove, :force_remove + +state_attrs :aws_access_key, + :resource_id, + :tags + +attribute :aws_access_key, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :resource_id, :kind_of => [ String, Array ], :regex => /(i|snap|vol)-[a-zA-Z0-9]+/ +attribute :tags, :kind_of => Hash, :required => true diff --git a/aws/resources/s3_file.rb b/aws/resources/s3_file.rb new file mode 100644 index 0000000..9f8f20d --- /dev/null +++ b/aws/resources/s3_file.rb @@ -0,0 +1,42 @@ +actions :create, :create_if_missing, :touch, :delete + +state_attrs :aws_access_key_id, + :backup, + :bucket, + :checksum, + :group, + :mode, + :owner, + :path, + :remote_path + +attribute :path, :kind_of => String, :name_attribute => true +attribute :remote_path, :kind_of => String +attribute :bucket, :kind_of => String +attribute :aws_access_key_id, :kind_of => String +attribute :aws_secret_access_key, :kind_of => String +attribute :owner, :regex => Chef::Config[:user_valid_regex] +attribute :group, :regex => Chef::Config[:group_valid_regex] +attribute :mode, :kind_of => [String, NilClass], :default => nil +attribute :checksum, :kind_of => [String, NilClass], :default => nil +attribute :backup, :kind_of => [Integer, FalseClass], :default => 5 +if node['platform_family'] == "windows" + attribute :inherits, :kind_of => [TrueClass, FalseClass], :default => true + attribute :rights, :kind_of => Hash, :default => nil +end + +version = Chef::Version.new(Chef::VERSION[/^(\d+\.\d+\.\d+)/, 1]) +if version.major > 11 || (version.major == 11 && version.minor >= 6) + attribute :headers, :kind_of => Hash, :default => nil + attribute :use_etag, :kind_of => [TrueClass, FalseClass], :default => true + attribute :use_last_modified, :kind_of => [TrueClass, FalseClass], :default => true + attribute :atomic_update, :kind_of => [TrueClass, FalseClass], :default => true + attribute :force_unlink, :kind_of => [TrueClass, FalseClass], :default => false + attribute :manage_symlink_source, :kind_of => [TrueClass, FalseClass], :default => nil +end + +def initialize(*args) + super + @action = :create + @path = name +end diff --git a/chef_handler/CHANGELOG.md b/chef_handler/CHANGELOG.md new file mode 100644 index 0000000..ed380f4 --- /dev/null +++ b/chef_handler/CHANGELOG.md @@ -0,0 +1,44 @@ +chef_handler cookbook CHANGELOG +=============================== + +v1.1.6 (2014-04-09) +------------------- +[COOK-4494] - Add ChefSpec matchers + + +v1.1.5 (2014-02-25) +------------------- +- [COOK-4117] - use the correct scope when searching the children class name + + +v1.1.4 +------ +- [COOK-2146] - style updates + +v1.1.2 +--------- +- [COOK-1989] - fix scope for handler local variable to the enable block + +v1.1.0 +------ + +- [COOK-1645] - properly delete old handlers +- [COOK-1322] - support platforms that use 'wheel' as root group' + +v1.0.8 +------ +- [COOK-1177] - doesn't work on windows due to use of unix specific attributes + +v1.0.6 +------ +- [COOK-1069] - typo in chef_handler readme + +v1.0.4 +------ +- [COOK-654] dont try and access a class before it has been loaded +- fix bad boolean check (if vs unless) + +v1.0.2 +------ +- [COOK-620] ensure handler code is reloaded during daemonized chef runs + diff --git a/chef_handler/README.md b/chef_handler/README.md new file mode 100644 index 0000000..06f9347 --- /dev/null +++ b/chef_handler/README.md @@ -0,0 +1,103 @@ +Description +=========== + +Creates a configured handler path for distributing [Chef report and exception handlers](http://docs.opscode.com/handlers.html). Also exposes an LWRP for enabling Chef handlers from within recipe code (as opposed to hard coding in the client.rb file). This is useful for cookbook authors who may want to ship a product specific handler (see the `cloudkick` cookbook for an example) with their cookbook. + +Attributes +========== + +`node["chef_handler"]["handler_path"]` - location to drop off handlers directory, default is `/var/chef/handlers`. + +Resource/Provider +================= + +`chef_handler` +-------------- + +Requires, configures and enables handlers on the node for the current Chef run. Also has the ability to pass arguments to the handlers initializer. This allows initialization data to be pulled from a node's attribute data. + +It is best to declare `chef_handler` resources early on in the compile phase so they are available to fire for any exceptions during the Chef run. If you have a base role you would want any recipes that register Chef handlers to come first in the run_list. + +### Actions + +- :enable: Enables the Chef handler for the current Chef run on the current node +- :disable: Disables the Chef handler for the current Chef run on the current node + +### Attribute Parameters + +- class_name: name attribute. The name of the handler class (can be module name-spaced). +- source: full path to the handler file. can also be a gem path if the handler ships as part of a Ruby gem. +- arguments: an array of arguments to pass the handler's class initializer +- supports: type of Chef Handler to register as, ie :report, :exception or both. default is `:report => true, :exception => true` + +### Example + + # register the Chef::Handler::JsonFile handler + # that ships with the Chef gem + chef_handler "Chef::Handler::JsonFile" do + source "chef/handler/json_file" + arguments :path => '/var/chef/reports' + action :enable + end + + # do the same but during the compile phase + chef_handler "Chef::Handler::JsonFile" do + source "chef/handler/json_file" + arguments :path => '/var/chef/reports' + action :nothing + end.run_action(:enable) + + # handle exceptions only + chef_handler "Chef::Handler::JsonFile" do + source "chef/handler/json_file" + arguments :path => '/var/chef/reports' + supports :exception => true + action :enable + end + + + # enable the CloudkickHandler which was + # dropped off in the default handler path. + # passes the oauth key/secret to the handler's + # intializer. + chef_handler "CloudkickHandler" do + source "#{node['chef_handler']['handler_path']}/cloudkick_handler.rb" + arguments [node['cloudkick']['oauth_key'], node['cloudkick']['oauth_secret']] + action :enable + end + + +Usage +===== + +default +------- + +Put the recipe `chef_handler` at the start of the node's run list to make sure that custom handlers are dropped off early on in the Chef run and available for later recipes. + +For information on how to write report and exception handlers for Chef, please see the Chef wiki pages: +http://wiki.opscode.com/display/chef/Exception+and+Report+Handlers + +json_file +--------- + +Leverages the `chef_handler` LWRP to automatically register the `Chef::Handler::JsonFile` handler that ships as part of Chef. This handler serializes the run status data to a JSON file located at `/var/chef/reports`. + +License and Author +================== + +Author:: Seth Chisamore () + +Copyright:: 2011, Opscode, Inc + +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/chef_handler/attributes/default.rb b/chef_handler/attributes/default.rb new file mode 100644 index 0000000..19d2fec --- /dev/null +++ b/chef_handler/attributes/default.rb @@ -0,0 +1,30 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: chef_handlers +# Attribute:: default +# +# Copyright 2011-2013, Opscode, Inc +# +# 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. +# + +default["chef_handler"]["root_user"] = "root" + +case platform +when "openbsd", "freebsd", "mac_os_x", "mac_os_x_server" + default["chef_handler"]["root_group"] = "wheel" +else + default["chef_handler"]["root_group"] = "root" +end + +default["chef_handler"]["handler_path"] = "#{File.expand_path(File.join(Chef::Config[:file_cache_path], '..'))}/handlers" diff --git a/chef_handler/files/default/handlers/README b/chef_handler/files/default/handlers/README new file mode 100644 index 0000000..b575066 --- /dev/null +++ b/chef_handler/files/default/handlers/README @@ -0,0 +1 @@ +This directory contains Chef handlers to distribute to your nodes. diff --git a/chef_handler/libraries/matchers.rb b/chef_handler/libraries/matchers.rb new file mode 100644 index 0000000..bfebc33 --- /dev/null +++ b/chef_handler/libraries/matchers.rb @@ -0,0 +1,29 @@ +# +# Author:: Douglas Thrift () +# Cookbook Name:: chef_handler +# Library:: matchers +# +# Copyright 2014, Chef Software, Inc. +# +# 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. +# + +if defined?(ChefSpec) + def enable_chef_handler(resource_name) + ChefSpec::Matchers::ResourceMatcher.new(:chef_handler, :enable, resource_name) + end + + def disable_chef_handler(resource_name) + ChefSpec::Matchers::ResourceMatcher.new(:chef_handler, :disable, resource_name) + end +end diff --git a/chef_handler/metadata.json b/chef_handler/metadata.json new file mode 100644 index 0000000..274c87f --- /dev/null +++ b/chef_handler/metadata.json @@ -0,0 +1,29 @@ +{ + "name": "chef_handler", + "version": "1.1.6", + "description": "Distribute and enable Chef Exception and Report handlers", + "long_description": "Description\n===========\n\nCreates a configured handler path for distributing [Chef report and exception handlers](http://docs.opscode.com/handlers.html). Also exposes an LWRP for enabling Chef handlers from within recipe code (as opposed to hard coding in the client.rb file). This is useful for cookbook authors who may want to ship a product specific handler (see the `cloudkick` cookbook for an example) with their cookbook.\n\nAttributes\n==========\n\n`node[\"chef_handler\"][\"handler_path\"]` - location to drop off handlers directory, default is `/var/chef/handlers`.\n\nResource/Provider\n=================\n\n`chef_handler`\n--------------\n\nRequires, configures and enables handlers on the node for the current Chef run. Also has the ability to pass arguments to the handlers initializer. This allows initialization data to be pulled from a node's attribute data.\n\nIt is best to declare `chef_handler` resources early on in the compile phase so they are available to fire for any exceptions during the Chef run. If you have a base role you would want any recipes that register Chef handlers to come first in the run_list.\n\n### Actions\n\n- :enable: Enables the Chef handler for the current Chef run on the current node\n- :disable: Disables the Chef handler for the current Chef run on the current node\n\n### Attribute Parameters\n\n- class_name: name attribute. The name of the handler class (can be module name-spaced).\n- source: full path to the handler file. can also be a gem path if the handler ships as part of a Ruby gem.\n- arguments: an array of arguments to pass the handler's class initializer\n- supports: type of Chef Handler to register as, ie :report, :exception or both. default is `:report => true, :exception => true`\n\n### Example\n\n # register the Chef::Handler::JsonFile handler\n # that ships with the Chef gem\n chef_handler \"Chef::Handler::JsonFile\" do\n source \"chef/handler/json_file\"\n arguments :path => '/var/chef/reports'\n action :enable\n end\n\n # do the same but during the compile phase\n chef_handler \"Chef::Handler::JsonFile\" do\n source \"chef/handler/json_file\"\n arguments :path => '/var/chef/reports'\n action :nothing\n end.run_action(:enable)\n\n # handle exceptions only\n chef_handler \"Chef::Handler::JsonFile\" do\n source \"chef/handler/json_file\"\n arguments :path => '/var/chef/reports'\n supports :exception => true\n action :enable\n end\n\n\n # enable the CloudkickHandler which was\n # dropped off in the default handler path.\n # passes the oauth key/secret to the handler's\n # intializer.\n chef_handler \"CloudkickHandler\" do\n source \"#{node['chef_handler']['handler_path']}/cloudkick_handler.rb\"\n arguments [node['cloudkick']['oauth_key'], node['cloudkick']['oauth_secret']]\n action :enable\n end\n\n\nUsage\n=====\n\ndefault\n-------\n\nPut the recipe `chef_handler` at the start of the node's run list to make sure that custom handlers are dropped off early on in the Chef run and available for later recipes.\n\nFor information on how to write report and exception handlers for Chef, please see the Chef wiki pages:\nhttp://wiki.opscode.com/display/chef/Exception+and+Report+Handlers\n\njson_file\n---------\n\nLeverages the `chef_handler` LWRP to automatically register the `Chef::Handler::JsonFile` handler that ships as part of Chef. This handler serializes the run status data to a JSON file located at `/var/chef/reports`.\n\nLicense and Author\n==================\n\nAuthor:: Seth Chisamore ()\n\nCopyright:: 2011, Opscode, Inc\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n", + "maintainer": "Opscode, Inc.", + "maintainer_email": "cookbooks@opscode.com", + "license": "Apache 2.0", + "platforms": { + }, + "dependencies": { + }, + "recommendations": { + }, + "suggestions": { + }, + "conflicting": { + }, + "providing": { + }, + "replacing": { + }, + "attributes": { + }, + "groupings": { + }, + "recipes": { + } +} \ No newline at end of file diff --git a/chef_handler/providers/default.rb b/chef_handler/providers/default.rb new file mode 100644 index 0000000..aa2ee55 --- /dev/null +++ b/chef_handler/providers/default.rb @@ -0,0 +1,97 @@ +# +# Author:: Seth Chisamore +# Cookbook Name:: chef_handler +# Provider:: default +# +# Copyright:: 2011-2013, Opscode, Inc +# +# 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. +# + +def whyrun_supported? + true +end + +action :enable do + # use load instead of require to ensure the handler file + # is reloaded into memory each chef run. fixes COOK-620 + handler = nil + converge_by("load #{@new_resource.source}") do + begin + Object.send(:remove_const, klass) + GC.start + rescue + Chef::Log.debug("#{@new_resource.class_name} has not been loaded.") + end + file_name = @new_resource.source + file_name << ".rb" unless file_name =~ /.*\.rb$/ + load file_name + handler = klass.send(:new, *collect_args(@new_resource.arguments)) + end + @new_resource.supports.each do |type, enable| + if enable + # we have to re-enable the handler every chef run + # to ensure daemonized Chef always has the latest + # handler code. TODO: add a :reload action + converge_by("enable #{@new_resource} as a #{type} handler") do + Chef::Log.info("Enabling #{@new_resource} as a #{type} handler") + Chef::Config.send("#{type.to_s}_handlers").delete_if { |v| v.class.to_s.include? @new_resource.class_name.split('::', 3).last } + Chef::Config.send("#{type.to_s}_handlers") << handler + end + end + end +end + +action :disable do + @new_resource.supports.each_key do |type| + if enabled?(type) + converge_by("disable #{@new_resource} as a #{type} handler") do + Chef::Log.info("Disabling #{@new_resource} as a #{type} handler") + Chef::Config.send("#{type.to_s}_handlers").delete_if { |v| v.class.to_s.include? @new_resource.class_name.split('::', 3).last } + end + end + end +end + +def load_current_resource + @current_resource = Chef::Resource::ChefHandler.new(@new_resource.name) + @current_resource.class_name(@new_resource.class_name) + @current_resource.source(@new_resource.source) + @current_resource +end + +private + +def enabled?(type) + Chef::Config.send("#{type.to_s}_handlers").select do |handler| + handler.class.to_s.include? @new_resource.class_name + end.size >= 1 +end + +def collect_args(resource_args = []) + if resource_args.is_a? Array + resource_args + else + [resource_args] + end +end + +def klass + @klass ||= begin + # we need to search the ancestors only for the + # first/uppermost namespace of the class, so we need + # to enable the #const_get inherit paramenter only when + # we are searching in Kernel scope (see COOK-4117). + @new_resource.class_name.split('::').inject(Kernel) { |scope, const_name| scope.const_get(const_name, scope === Kernel) } + end +end diff --git a/chef_handler/recipes/default.rb b/chef_handler/recipes/default.rb new file mode 100644 index 0000000..540a5ff --- /dev/null +++ b/chef_handler/recipes/default.rb @@ -0,0 +1,33 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: chef_handlers +# Recipe:: default +# +# Copyright 2011, Opscode, Inc. +# +# 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. +# + +Chef::Log.info("Chef Handlers will be at: #{node['chef_handler']['handler_path']}") + +remote_directory node['chef_handler']['handler_path'] do + source 'handlers' + # Just inherit permissions on Windows, don't try to set POSIX perms + if node["platform"] != "windows" + owner node['chef_handler']['root_user'] + group node['chef_handler']['root_group'] + mode "0755" + recursive true + end + action :nothing +end.run_action(:create) diff --git a/chef_handler/recipes/json_file.rb b/chef_handler/recipes/json_file.rb new file mode 100644 index 0000000..d2fab10 --- /dev/null +++ b/chef_handler/recipes/json_file.rb @@ -0,0 +1,28 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: chef_handlers +# Recipe:: json_file +# +# Copyright 2011, Opscode, Inc. +# +# 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. +# + +# force resource actions in compile phase so exception handler +# fires for compile phase exceptions + +chef_handler "Chef::Handler::JsonFile" do + source "chef/handler/json_file" + arguments :path => '/var/chef/reports' + action :nothing +end.run_action(:enable) diff --git a/chef_handler/resources/default.rb b/chef_handler/resources/default.rb new file mode 100644 index 0000000..f74aafa --- /dev/null +++ b/chef_handler/resources/default.rb @@ -0,0 +1,34 @@ +# +# Author:: Seth Chisamore +# Cookbook Name:: chef_handler +# Resource:: default +# +# Copyright:: 2011-2013, Opscode, Inc +# +# 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. +# + +actions :enable, :disable + +attribute :class_name, :kind_of => String, :name_attribute => true +attribute :source, :default => nil, :kind_of => String +attribute :arguments, :default => [] +attribute :supports, :kind_of => Hash, :default => { :report => true, :exception => true } + +# we have to set default for the supports attribute +# in initializer since it is a 'reserved' attribute name +def initialize(*args) + super + @action = :enable + @supports = { :report => true, :exception => true } +end diff --git a/java/CHANGELOG.md b/java/CHANGELOG.md new file mode 100644 index 0000000..315895d --- /dev/null +++ b/java/CHANGELOG.md @@ -0,0 +1,164 @@ +Java Cookbook CHANGELOG +======================= +This file is used to list changes made in each version of the Java cookbook. + + +v1.14.0 +------- +### Bug +- **[COOK-3704](https://tickets.opscode.com/browse/COOK-3704)** - Fix alternatives when the package is already installed +- **[COOK-3668](https://tickets.opscode.com/browse/COOK-3668)** - Fix a condition that would result in an error executing action `run` on resource 'bash[update-java-alternatives]' +- **[COOK-3569](https://tickets.opscode.com/browse/COOK-3569)** - Fix bad checksum length +- **[COOK-3541](https://tickets.opscode.com/browse/COOK-3541)** - Fix an issue where Java cookbook installs both JDK 6 and JDK 7 when JDK 7 is specified +- **[COOK-3518](https://tickets.opscode.com/browse/COOK-3518)** - Allow Windoes recipe to download from signed S3 url +- **[COOK-2996](https://tickets.opscode.com/browse/COOK-2996)** - Fix a failure on Centos 6.4 and Oracle JDK 7 + +### Improvement +- **[COOK-2793](https://tickets.opscode.com/browse/COOK-2793)** - Improve Windows support + + +v1.13.0 +------- +### Bug +- **[COOK-3295](https://tickets.opscode.com/browse/COOK-3295)** - Add default `platform_family` option in Java helper +- **[COOK-3277](https://tickets.opscode.com/browse/COOK-3277)** - Fix support for Fedora + +### Improvement +- **[COOK-3278](https://tickets.opscode.com/browse/COOK-3278)** - Upgrade to Oracle Java 7u25 +- **[COOK-3029](https://tickets.opscode.com/browse/COOK-3029)** - Add Oracle RPM support +- **[COOK-2931](https://tickets.opscode.com/browse/COOK-2931)** - Add support for the platform `xenserver` +- **[COOK-2154](https://tickets.opscode.com/browse/COOK-2154)** - Add SmartOS support + +v1.12.0 +------- +### Improvement +- [COOK-2154]: Add SmartOS support to java::openjdk recipe +- [COOK-3278]: upgrade to Oracle Java 7u25 + +### Bug +- [COOK-2931]: Adding support for the platform 'xenserver' (for installations of java in DOM0) +- [COOK-3277]: java cookbook fails on Fedora + +v1.11.6 +------- +### Bug +- [COOK-2847]: Java cookbook does not have opensuse support +- [COOK-3142]: Syntax Errors spec/default_spec.rb:4-8 + +v1.11.4 +------- +### Bug +- [COOK-2989]: `bash[update-java-alternatives]` resource uses wrong attribute + +v1.11.2 +------- +### Bug +- Use SHA256 checksums for Oracle downloads, not SHA1. + +v1.11.0 +------- +This version brings a wealth of tests and (backwards-compatible) refactoring, plus some new features (updated Java, IBM recipe). + +### Sub-task +- [COOK-2897]: Add ibm recipe to java cookbook +- [COOK-2903]: move java_home resources to their own recipe +- [COOK-2904]: refactor ruby_block "update-java-alternatives" +- [COOK-2905]: use platform_family in java cookbook +- [COOK-2920]: add chefspec to java cookbook + +### Task +- [COOK-2902]: Refactor java cookbook + +### Improvement +- [COOK-2900]: update JDK to JDK 7u21, 6u45 + +v1.10.2 +------- +- [COOK-2415] - Fixed deprecation warnings in ark provider and openjdk recipe by using Chef::Mixin::ShellOut instead of Chef::ShellOut + +v1.10.0 +------- +- [COOK-2400] - Allow java ark :url to be https +- [COOK-2436] - Upgrade needed for oracle jdk in java cookbook + +v1.9.6 +------ +- [COOK-2412] - add support for Oracle Linux + +v1.9.4 +------ +- [COOK-2083] - Run set-env-java-home in Java cookbook only if necessary +- [COOK-2332] - ark provider does not allow for *.tgz tarballs to be used +- [COOK-2345] - Java cookbook fails on CentOS6 (update-java-alternatives) + +v1.9.2 +------ +- [COOK-2306] - FoodCritic fixes for java cookbook + +v1.9.0 +------ +- [COOK-2236] - Update the Oracle Java version in the Java cookbook to release 1.7u11 + +v1.8.2 +------ +- [COOK-2205] - Fix for missing /usr/lib/jvm/default-java on Debian + +v1.8.0 +------ +- [COOK-2095] - Add windows support + +v1.7.0 +------ +- [COOK-2001] - improvements for Oracle update-alternatives + - When installing an Oracle JDK it is now registered with a higher + priority than OpenJDK. (Related to COOK-1131.) + - When running both the oracle and oracle_i386 recipes, alternatives + are now created for both JDKs. + - Alternatives are now created for all binaries listed in version + specific attributes. (Related to COOK-1563 and COOK-1635.) + - When installing Oracke JDKs on Ubuntu, create .jinfo files for use + with update-java-alternatives. Commands to set/install + alternatives now only run if needed. + +v1.6.4 +------ +- [COOK-1930] - fixed typo in attribute for java 5 on i586 + +v1.6.2 +------ +- whyrun support in `java_ark` LWRP +- CHEF-1804 compatibility +- [COOK-1786]- install Java 6u37 and Java 7u9 +- [COOK-1819] -incorrect warning text about `node['java']['oracle']['accept_oracle_download_terms']` + +v1.6.0 +------ +- [COOK-1218] - Install Oracle JDK from Oracle download directly +- [COOK-1631] - set JAVA_HOME in openjdk recipe +- [COOK-1655] - Install correct architecture on Amazon Linux + +v1.5.4 +------ +- [COOK-885] - update alternatives called on wrong file +- [COOK-1607] - use shellout instead of execute resource to update alternatives + +v1.5.2 +------ +- [COOK-1200] - remove sun-java6-jre on Ubuntu before installing Oracle's Java +- [COOK-1260] - fails on Ubuntu 12.04 64bit with openjdk7 +- [COOK-1265] - Oracle Java should symlink the jar command + +v1.5.0 +------ +- [COOK-1146] - Oracle now prevents download of JDK via non-browser +- [COOK-1114] - fix File.exists? + +v1.4.2 +------ +- [COOK-1051] - fix attributes typo and platform case switch consistency + +v1.4.0 +------ +- [COOK-858] - numerous updates: handle jdk6 and 7, switch from sun to oracle, make openjdk default, add `java_ark` LWRP. +- [COOK-942] - FreeBSD support +- [COOK-520] - ArchLinux support diff --git a/java/README.md b/java/README.md new file mode 100644 index 0000000..a293dc6 --- /dev/null +++ b/java/README.md @@ -0,0 +1,295 @@ +Description +=========== + +This cookbook installs a Java JDK/JRE. It defaults to installing +OpenJDK, but it can also install Oracle and IBM JDKs. + +**IMPORTANT NOTE** + +As of 26 March 2012 you can no longer directly download the JDK from +Oracle's website without using a special cookie. This cookbook uses +that cookie to download the oracle recipe on your behalf, however the +`java::oracle` recipe forces you to set either override the +`node['java']['oracle']['accept_oracle_download_terms']` to true or +set up a private repository accessible by HTTP. + +### Example + +override the `accept_oracle_download_terms` in, e.g., `roles/base.rb` + + default_attributes( + :java => { + :oracle => { + "accept_oracle_download_terms" => true + } + } + ) + +Requirements +============ + +Chef 0.10.10+ and Ohai 6.10+ for `platform_family` use. + +## Platform + +* Debian, Ubuntu +* CentOS, Red Hat, Fedora, Scientific, Amazon, XenServer +* ArchLinux +* FreeBSD +* SmartOS +* Windows + +This cookbook includes cross-platform testing support via +`test-kitchen`, see `TESTING.md`. + +Attributes +========== + +See `attributes/default.rb` for default values. + +* `node['java']['remove_deprecated_packages']` - Removes the now +deprecated Ubuntu JDK packages from the system, default `false` +* `node['java']['install_flavor']` - Flavor of JVM you would like +installed (`oracle`, `openjdk`, `ibm`, `windows`), default `openjdk` +on Linux/Unix platforms, `windows` on Windows platforms. +* `node['java']['jdk_version']` - JDK version to install, defaults to + `'6'`. +* `node['java']['java_home']` - Default location of the + "`$JAVA_HOME`". +* `node['java']['openjdk_packages']` - Array of OpenJDK package names + to install in the `java::openjdk` recipe. This is set based on the + platform. +* `node['java']['tarball']` - Name of the tarball to retrieve from +your internal repository, default `jdk1.6.0_29_i386.tar.gz` +* `node['java']['tarball_checksum']` - Checksum for the tarball, if +you use a different tarball, you also need to create a new sha256 +checksum +* `node['java']['jdk']` - Version and architecture specific attributes +for setting the URL on Oracle's site for the JDK, and the checksum of +the .tar.gz. +* `node['java']['oracle']['accept_oracle_download_terms']` - Indicates + that you accept Oracle's EULA +* `node['java']['windows']['url']` - The internal location of your + java install for windows +* `node['java']['windows']['package_name']` - The package name used by + windows_package to check in the registry to determine if the install + has already been run +* `node['java']['windows']['checksum']` - The checksum for the package to + download on Windows machines (default is nil, which does not perform + checksum validation) +* `node['java']['ibm']['url']` - The URL which to download the IBM + JDK/SDK. See the `ibm` recipe section below. +* `node['java']['ibm']['accept_ibm_download_terms']` - Indicates that + you accept IBM's EULA (for `java::ibm`) +* `node['java']['accept_license_agreement']` - Indicates that you accept + the EULA for openjdk package installation. + +Recipes +======= + +## default + +Include the default recipe in a run list, to get `java`. By default +the `openjdk` flavor of Java is installed, but this can be changed by +using the `install_flavor` attribute. By default on Windows platform +systems, the `install_flavor` is `windows`. + +OpenJDK is the default because of licensing changes made upstream by +Oracle. See notes on the `oracle` recipe below. + +## openjdk + +This recipe installs the `openjdk` flavor of Java. It also uses the +`alternatives` system on RHEL/Debian families to set the default Java. + +On platforms such as SmartOS that require the acceptance of a license +agreement during package installation, set +`node['java']['accept_license_agreement']` to true in order to indicate +that you accept the license. + +## oracle + +This recipe installs the `oracle` flavor of Java. This recipe does not +use distribution packages as Oracle changed the licensing terms with +JDK 1.6u27 and prohibited the practice for both RHEL and Debian family +platforms. + +For both RHEL and Debian families, this recipe pulls the binary +distribution from the Oracle website, and installs it in the default +`JAVA_HOME` for each distribution. For Debian, this is +`/usr/lib/jvm/default-java`. For RHEl, this is `/usr/lib/jvm/java`. + +After putting the binaries in place, the `java::oracle` recipe updates +`/usr/bin/java` to point to the installed JDK using the +`update-alternatives` script. This is all handled in the `java_ark` +LWRP. + +## oracle_i386 + +This recipe installs the 32-bit Java virtual machine without setting +it as the default. This can be useful if you have applications on the +same machine that require different versions of the JVM. + +This recipe operates in a similar manner to `java::oracle`. + +## oracle_rpm + +This recipe installs the Oracle JRE or JDK provided by a custom YUM +repositories. +It also uses the `alternatives` system on RHEL families to set +the default Java. + +## windows + +Because there is no easy way to pull the java msi off oracle's site, +this recipe requires you to host it internally on your own http repo. + +## ibm + +The `java::ibm` recipe is used to install the IBM version of Java. +Note that IBM requires you to create an account *and* log in to +download the binary installer for your platform. You must accept the +license agreement with IBM to use their version of Java. In this +cookbook, you indicate this by setting +`node['java']['ibm']['accept_ibm_download_terms']` to `true`. You must +also host the binary on your own HTTP server to have an automated +installation. The `node['java']['ibm']['url']` attribute must be set +to a valid https/http URL; the URL is checked for validity in the recipe. + +At this time the `java::ibm` recipe does not support multiple SDK +installations. + +Resources/Providers +=================== + +This cookbook contains the `java_ark` LWRP. Generally speaking this +LWRP is deprecated in favor of `ark` from the +[ark cookbook](https://github.com/opscode-cookbooks/ark), but it is +still used in this cookbook for handling the Oracle JDK installation. + +By default, the extracted directory is extracted to +`app_root/extracted_dir_name` and symlinked to `app_root/default` + +## Actions + +- `:install`: extracts the tarball and makes necessary symlinks +- `:remove`: removes the tarball and run update-alternatives for all + symlinked `bin_cmds` + +## Attribute Parameters + +- `url`: path to tarball, .tar.gz, .bin (oracle-specific), and .zip + currently supported +- `checksum`: SHA256 checksum, not used for security but avoid + redownloading the archive on each chef-client run +- `app_home`: the default for installations of this type of + application, for example, `/usr/lib/tomcat/default`. If your + application is not set to the default, it will be placed at the same + level in the directory hierarchy but the directory name will be + `app_root/extracted_directory_name + "_alt"` +- `app_home_mode`: file mode for app_home, is an integer +- `bin_cmds`: array of binary commands that should be symlinked to + `/usr/bin`, examples are mvn, java, javac, etc. These cmds must be in + the `bin` subdirectory of the extracted folder. Will be ignored if this + `java_ark` is not the default +- `owner`: owner of extracted directory, set to "root" by default +- `default`: whether this the default installation of this package, + boolean true or false + +## Examples + + # install jdk6 from Oracle + java_ark "jdk" do + url 'http://download.oracle.com/otn-pub/java/jdk/6u29-b11/jdk-6u29-linux-x64.bin' + checksum 'a8603fa62045ce2164b26f7c04859cd548ffe0e33bfc979d9fa73df42e3b3365' + app_home '/usr/local/java/default' + bin_cmds ["java", "javac"] + action :install + end + +Usage +===== + +Simply include the `java` recipe where ever you would like Java installed. + +To install Oracle flavored Java override the `node['java']['install_flavor']` attribute with in role: + + name "java" + description "Install Oracle Java on Ubuntu" + default_attributes( + "java" => { + "install_flavor" => "oracle" + } + ) + run_list( + "recipe[java]" + ) + +To install IBM flavored Java, set the required attributes: + + name "java" + description "Install IBM Java on Ubuntu" + default_attributes( + "java" => { + "install_flavor" => "ibm", + "ibm" => { + "accept_ibm_download_terms" => true, + "url" => "http://fileserver.example.com/ibm-java-x86_64-sdk-7.0-4.1.bin", + "checksum" => "The SHA256 checksum of the bin" + } + } + ) + run_list( + "recipe[java]" + ) + + +Development +=========== + +This cookbook uses +[test-kitchen](https://github.com/opscode/test-kitchen) for +integration tests and +[ChefSpec/RSpec](https://github.com/acrmp/chefspec) for unit tests. +Pull requests should pass existing tests in +`files/default/tests/minitest-handler`. + +At this time due to licensing concerns, the IBM recipe is not set up +in test kitchen. If you would like to test this locally, copy +.kitchen.yml to .kitchen.local.yml and add the following suite: + + suites: + - name: ibm + run_list: ["recipe[java]"] + attributes: + java: + install_flavor: "ibm" + ibm: + accept_ibm_download_terms: true + url: "http://jenkins/ibm-java-x86_64-sdk-7.0-4.1.bin" + checksum: the-sha256-checksum + +Log into the IBM DeveloperWorks site to download a copy of the IBM +Java SDK you wish to use/test, host it on an internal HTTP server, and +calculate the SHA256 checksum to use in the suite. + +License and Author +================== + +* Author: Seth Chisamore () +* Author: Bryan W. Berry () +* Author: Joshua Timberman () + +Copyright: 2008-2013, Opscode, Inc + +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/java/attributes/default.rb b/java/attributes/default.rb new file mode 100644 index 0000000..d7f0121 --- /dev/null +++ b/java/attributes/default.rb @@ -0,0 +1,101 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: java +# Attributes:: default +# +# Copyright 2010, Opscode, Inc. +# +# 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. + +# remove the deprecated Ubuntu jdk packages +default['java']['remove_deprecated_packages'] = false + +# default jdk attributes +default['java']['install_flavor'] = "openjdk" +default['java']['jdk_version'] = '6' +default['java']['arch'] = kernel['machine'] =~ /x86_64/ ? "x86_64" : "i586" +default['java']['openjdk_packages'] = [] +default['java']['accept_license_agreement'] = false + +case node['platform_family'] +when "rhel", "fedora" + default['java']['java_home'] = "/usr/lib/jvm/java" + default['java']['openjdk_packages'] = ["java-1.#{node['java']['jdk_version']}.0-openjdk", "java-1.#{node['java']['jdk_version']}.0-openjdk-devel"] +when "freebsd" + default['java']['java_home'] = "/usr/local/openjdk#{node['java']['jdk_version']}" + default['java']['openjdk_packages'] = ["openjdk#{node['java']['jdk_version']}"] +when "arch" + default['java']['java_home'] = "/usr/lib/jvm/java-#{node['java']['jdk_version']}-openjdk" + default['java']['openjdk_packages'] = ["openjdk#{node['java']['jdk_version']}}"] +when "windows" + default['java']['install_flavor'] = "windows" + default['java']['windows']['url'] = nil + default['java']['windows']['checksum'] = nil + default['java']['windows']['package_name'] = "Java(TM) SE Development Kit 7 (64-bit)" +when "debian" + default['java']['java_home'] = "/usr/lib/jvm/java-#{node['java']['jdk_version']}-#{node['java']['install_flavor']}-#{node['kernel']['machine'] == 'x86_64' ? 'amd64' : 'i386'}" + default['java']['openjdk_packages'] = ["openjdk-#{node['java']['jdk_version']}-jdk", "openjdk-#{node['java']['jdk_version']}-jre-headless"] +when "smartos" + default['java']['java_home'] = "/opt/local/java/sun6" + default['java']['openjdk_packages'] = ["sun-jdk#{node['java']['jdk_version']}", "sun-jre#{node['java']['jdk_version']}"] +else + default['java']['java_home'] = "/usr/lib/jvm/default-java" + default['java']['openjdk_packages'] = ["openjdk-#{node['java']['jdk_version']}-jdk"] +end + +case node['java']['install_flavor'] +when 'ibm' + default['java']['ibm']['url'] = nil + default['java']['ibm']['checksum'] = nil + default['java']['ibm']['accept_ibm_download_terms'] = false + default['java']['java_home'] = "/opt/ibm/java" +when 'oracle_rpm' + default['java']['oracle_rpm']['type'] = 'jdk' + default['java']['java_home'] = "/usr/java/latest" +end + +# if you change this to true, you can download directly from Oracle +default['java']['oracle']['accept_oracle_download_terms'] = false + +# direct download paths for oracle, you have been warned! + +# jdk6 attributes +default['java']['jdk']['6']['bin_cmds'] = [ "appletviewer", "apt", "ControlPanel", "extcheck", "HtmlConverter", "idlj", "jar", "jarsigner", + "java", "javac", "javadoc", "javah", "javap", "javaws", "jconsole", "jcontrol", "jdb", "jhat", + "jinfo", "jmap", "jps", "jrunscript", "jsadebugd", "jstack", "jstat", "jstatd", "jvisualvm", + "keytool", "native2ascii", "orbd", "pack200", "policytool", "rmic", "rmid", "rmiregistry", + "schemagen", "serialver", "servertool", "tnameserv", "unpack200", "wsgen", "wsimport", "xjc" ] + +# x86_64 +default['java']['jdk']['6']['x86_64']['url'] = 'http://download.oracle.com/otn-pub/java/jdk/6u45-b06/jdk-6u45-linux-x64.bin' +default['java']['jdk']['6']['x86_64']['checksum'] = '6b493aeab16c940cae9e3d07ad2a5c5684fb49cf06c5d44c400c7993db0d12e8' + +# i586 +default['java']['jdk']['6']['i586']['url'] = 'http://download.oracle.com/otn-pub/java/jdk/6u45-b06/jdk-6u45-linux-i586.bin' +default['java']['jdk']['6']['i586']['checksum'] = 'd53b5a2518d80e1d95565f0adda54eee229dc5f4a1d1a3c2f7bf5045b168a357' + +# jdk7 attributes + +default['java']['jdk']['7']['bin_cmds'] = [ "appletviewer", "apt", "ControlPanel", "extcheck", "idlj", "jar", "jarsigner", "java", "javac", + "javadoc", "javafxpackager", "javah", "javap", "javaws", "jcmd", "jconsole", "jcontrol", "jdb", + "jhat", "jinfo", "jmap", "jps", "jrunscript", "jsadebugd", "jstack", "jstat", "jstatd", "jvisualvm", + "keytool", "native2ascii", "orbd", "pack200", "policytool", "rmic", "rmid", "rmiregistry", + "schemagen", "serialver", "servertool", "tnameserv", "unpack200", "wsgen", "wsimport", "xjc" ] + +# x86_64 +default['java']['jdk']['7']['x86_64']['url'] = 'http://download.oracle.com/otn-pub/java/jdk/7u25-b15/jdk-7u25-linux-x64.tar.gz' +default['java']['jdk']['7']['x86_64']['checksum'] = 'f80dff0e19ca8d038cf7fe3aaa89538496b80950f4d10ff5f457988ae159b2a6' + +# i586 +default['java']['jdk']['7']['i586']['url'] = 'http://download.oracle.com/otn-pub/java/jdk/7u25-b15/jdk-7u25-linux-i586.tar.gz' +default['java']['jdk']['7']['i586']['checksum'] = 'dd89b20afa939992bb7fdc44837fa64f0a98d7ee1e5706fe8a2d9e2247ba6de7' diff --git a/java/files/default/tests/minitest/openjdk_test.rb b/java/files/default/tests/minitest/openjdk_test.rb new file mode 100644 index 0000000..353f159 --- /dev/null +++ b/java/files/default/tests/minitest/openjdk_test.rb @@ -0,0 +1,20 @@ +require 'minitest/spec' +require File.expand_path('../support/helpers', __FILE__) + +describe_recipe 'java::openjdk' do + include Helpers::Java + + it "installs the correct version of the jdk" do + java_version = shell_out("java -version") + version_line = java_version.stderr + jdk_version = version_line.scan(/\.([678])\./)[0][0] + assert_equal node['java']['jdk_version'], jdk_version + end + + it "properly sets JAVA_HOME environment variable" do + env_java_home = shell_out("echo $JAVA_HOME") + java_home = env_java_home.stdout.chomp + assert_equal node['java']['java_home'], java_home + end + +end diff --git a/java/files/default/tests/minitest/oracle_rpm_test.rb b/java/files/default/tests/minitest/oracle_rpm_test.rb new file mode 100644 index 0000000..9a7a87b --- /dev/null +++ b/java/files/default/tests/minitest/oracle_rpm_test.rb @@ -0,0 +1,19 @@ +require 'minitest/spec' +require File.expand_path('../support/helpers', __FILE__) + +describe_recipe 'java::oracle_rpm' do + include Helpers::Java + + it "installs the correct version of the jre/jdk" do + java_version = shell_out("java -version") + version_line = java_version.stderr + version_line.must_match /"#{node['java']['oracle_rpm']['version']}"/ + end + + it "properly sets JAVA_HOME environment variable" do + env_java_home = shell_out("echo $JAVA_HOME") + java_home = env_java_home.stdout.chomp + java_home.must_equal node['java']['java_home'] + end + +end diff --git a/java/files/default/tests/minitest/oracle_test.rb b/java/files/default/tests/minitest/oracle_test.rb new file mode 100644 index 0000000..24fed9d --- /dev/null +++ b/java/files/default/tests/minitest/oracle_test.rb @@ -0,0 +1,20 @@ +require 'minitest/spec' +require File.expand_path('../support/helpers', __FILE__) + +describe_recipe 'java::oracle' do + include Helpers::Java + + it "installs the correct version of the jdk" do + java_version = shell_out("java -version") + version_line = java_version.stderr + jdk_version = version_line.scan(/\.([678])\./)[0][0] + assert_equal node['java']['jdk_version'], jdk_version + end + + it "properly sets JAVA_HOME environment variable" do + env_java_home = shell_out("echo $JAVA_HOME") + java_home = env_java_home.stdout.chomp + assert_equal node['java']['java_home'], java_home + end + +end diff --git a/java/files/default/tests/minitest/support/helpers.rb b/java/files/default/tests/minitest/support/helpers.rb new file mode 100644 index 0000000..7d8946f --- /dev/null +++ b/java/files/default/tests/minitest/support/helpers.rb @@ -0,0 +1,29 @@ +# +# Cookbook:: java +# +# Author:: Joshua Timberman +# Copyright:: Copyright (c) 2013, Opscode, Inc. +# +# 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. +# + +module Helpers + module Java + require 'chef/mixin/shell_out' + include Chef::Mixin::ShellOut + include MiniTest::Chef::Assertions + include MiniTest::Chef::Context + include MiniTest::Chef::Resources + + end +end diff --git a/java/libraries/helpers.rb b/java/libraries/helpers.rb new file mode 100644 index 0000000..775aeb4 --- /dev/null +++ b/java/libraries/helpers.rb @@ -0,0 +1,101 @@ +# +# Author:: Joshua Timberman +# Copyright:: Copyright (c) 2013, Opscode, Inc. +# +# 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. +# + +require 'chef/version_constraint' +require 'uri' +require 'pathname' + +module Opscode + class OpenJDK + + attr_accessor :java_home, :jdk_version + + def initialize(node) + @node = node.to_hash + @java_home = @node['java']['java_home'] || '/usr/lib/jvm/default-java' + @jdk_version = @node['java']['jdk_version'] || '6' + end + + def java_location + File.join(java_home_parent(@java_home), openjdk_path, 'bin/java') + end + + def java_home_parent(java_home) + Pathname.new(java_home).parent.to_s + end + + def openjdk_path + case @node['platform_family'] + when 'debian' + 'java-%s-openjdk%s/jre' % [@jdk_version, arch_dir] + when 'rhel', 'fedora' + 'jre-1.%s.0-openjdk%s' % [@jdk_version, arch_dir] + when 'smartos' + 'jre' + else + 'jre' + end + end + + def arch_dir + @node['kernel']['machine'] == 'x86_64' ? sixty_four : thirty_two + end + + def sixty_four + case @node['platform_family'] + when 'debian' + old_version? ? '' : '-amd64' + when 'rhel', 'fedora' + '.x86_64' + else + '-x86_64' + end + end + + def thirty_two + case @node['platform_family'] + when 'debian' + old_version? ? '' : '-i386' + else + '' + end + end + + # This method is used above (#sixty_four, #thirty_two) so we know + # whether to specify the architecture as part of the path name. + def old_version? + case @node['platform'] + when 'ubuntu' + Chef::VersionConstraint.new("< 11.0").include?(@node['platform_version']) + when 'debian' + Chef::VersionConstraint.new("< 7.0").include?(@node['platform_version']) + end + end + end +end + +class Chef + class Recipe + def valid_ibm_jdk_uri?(url) + url =~ ::URI::ABS_URI && %w[http https].include?(::URI.parse(url).scheme) + end + + def platform_requires_license_acceptance? + %w(smartos).include?(node.platform) + end + end +end diff --git a/java/metadata.json b/java/metadata.json new file mode 100644 index 0000000..194b11d --- /dev/null +++ b/java/metadata.json @@ -0,0 +1,50 @@ +{ + "name": "java", + "version": "1.14.0", + "description": "Installs Java runtime.", + "long_description": "Description\n===========\n\nThis cookbook installs a Java JDK/JRE. It defaults to installing\nOpenJDK, but it can also install Oracle and IBM JDKs.\n\n**IMPORTANT NOTE**\n\nAs of 26 March 2012 you can no longer directly download the JDK from\nOracle's website without using a special cookie. This cookbook uses\nthat cookie to download the oracle recipe on your behalf, however the\n`java::oracle` recipe forces you to set either override the\n`node['java']['oracle']['accept_oracle_download_terms']` to true or\nset up a private repository accessible by HTTP.\n\n### Example\n\noverride the `accept_oracle_download_terms` in, e.g., `roles/base.rb`\n\n default_attributes(\n :java => {\n :oracle => {\n \"accept_oracle_download_terms\" => true\n }\n }\n )\n\nRequirements\n============\n\nChef 0.10.10+ and Ohai 6.10+ for `platform_family` use.\n\n## Platform\n\n* Debian, Ubuntu\n* CentOS, Red Hat, Fedora, Scientific, Amazon, XenServer\n* ArchLinux\n* FreeBSD\n* SmartOS\n* Windows\n\nThis cookbook includes cross-platform testing support via\n`test-kitchen`, see `TESTING.md`.\n\nAttributes\n==========\n\nSee `attributes/default.rb` for default values.\n\n* `node['java']['remove_deprecated_packages']` - Removes the now\ndeprecated Ubuntu JDK packages from the system, default `false`\n* `node['java']['install_flavor']` - Flavor of JVM you would like\ninstalled (`oracle`, `openjdk`, `ibm`, `windows`), default `openjdk`\non Linux/Unix platforms, `windows` on Windows platforms.\n* `node['java']['jdk_version']` - JDK version to install, defaults to\n `'6'`.\n* `node['java']['java_home']` - Default location of the\n \"`$JAVA_HOME`\".\n* `node['java']['openjdk_packages']` - Array of OpenJDK package names\n to install in the `java::openjdk` recipe. This is set based on the\n platform.\n* `node['java']['tarball']` - Name of the tarball to retrieve from\nyour internal repository, default `jdk1.6.0_29_i386.tar.gz`\n* `node['java']['tarball_checksum']` - Checksum for the tarball, if\nyou use a different tarball, you also need to create a new sha256\nchecksum\n* `node['java']['jdk']` - Version and architecture specific attributes\nfor setting the URL on Oracle's site for the JDK, and the checksum of\nthe .tar.gz.\n* `node['java']['oracle']['accept_oracle_download_terms']` - Indicates\n that you accept Oracle's EULA\n* `node['java']['windows']['url']` - The internal location of your\n java install for windows\n* `node['java']['windows']['package_name']` - The package name used by\n windows_package to check in the registry to determine if the install\n has already been run\n* `node['java']['windows']['checksum']` - The checksum for the package to\n download on Windows machines (default is nil, which does not perform\n checksum validation)\n* `node['java']['ibm']['url']` - The URL which to download the IBM\n JDK/SDK. See the `ibm` recipe section below.\n* `node['java']['ibm']['accept_ibm_download_terms']` - Indicates that\n you accept IBM's EULA (for `java::ibm`)\n* `node['java']['accept_license_agreement']` - Indicates that you accept\n the EULA for openjdk package installation.\n\nRecipes\n=======\n\n## default\n\nInclude the default recipe in a run list, to get `java`. By default\nthe `openjdk` flavor of Java is installed, but this can be changed by\nusing the `install_flavor` attribute. By default on Windows platform\nsystems, the `install_flavor` is `windows`.\n\nOpenJDK is the default because of licensing changes made upstream by\nOracle. See notes on the `oracle` recipe below.\n\n## openjdk\n\nThis recipe installs the `openjdk` flavor of Java. It also uses the\n`alternatives` system on RHEL/Debian families to set the default Java.\n\nOn platforms such as SmartOS that require the acceptance of a license\nagreement during package installation, set\n`node['java']['accept_license_agreement']` to true in order to indicate\nthat you accept the license.\n\n## oracle\n\nThis recipe installs the `oracle` flavor of Java. This recipe does not\nuse distribution packages as Oracle changed the licensing terms with\nJDK 1.6u27 and prohibited the practice for both RHEL and Debian family\nplatforms.\n\nFor both RHEL and Debian families, this recipe pulls the binary\ndistribution from the Oracle website, and installs it in the default\n`JAVA_HOME` for each distribution. For Debian, this is\n`/usr/lib/jvm/default-java`. For RHEl, this is `/usr/lib/jvm/java`.\n\nAfter putting the binaries in place, the `java::oracle` recipe updates\n`/usr/bin/java` to point to the installed JDK using the\n`update-alternatives` script. This is all handled in the `java_ark`\nLWRP.\n\n## oracle_i386\n\nThis recipe installs the 32-bit Java virtual machine without setting\nit as the default. This can be useful if you have applications on the\nsame machine that require different versions of the JVM.\n\nThis recipe operates in a similar manner to `java::oracle`.\n\n## oracle_rpm\n\nThis recipe installs the Oracle JRE or JDK provided by a custom YUM\nrepositories.\nIt also uses the `alternatives` system on RHEL families to set\nthe default Java.\n\n## windows\n\nBecause there is no easy way to pull the java msi off oracle's site,\nthis recipe requires you to host it internally on your own http repo.\n\n## ibm\n\nThe `java::ibm` recipe is used to install the IBM version of Java.\nNote that IBM requires you to create an account *and* log in to\ndownload the binary installer for your platform. You must accept the\nlicense agreement with IBM to use their version of Java. In this\ncookbook, you indicate this by setting\n`node['java']['ibm']['accept_ibm_download_terms']` to `true`. You must\nalso host the binary on your own HTTP server to have an automated\ninstallation. The `node['java']['ibm']['url']` attribute must be set\nto a valid https/http URL; the URL is checked for validity in the recipe.\n\nAt this time the `java::ibm` recipe does not support multiple SDK\ninstallations.\n\nResources/Providers\n===================\n\nThis cookbook contains the `java_ark` LWRP. Generally speaking this\nLWRP is deprecated in favor of `ark` from the\n[ark cookbook](https://github.com/opscode-cookbooks/ark), but it is\nstill used in this cookbook for handling the Oracle JDK installation.\n\nBy default, the extracted directory is extracted to\n`app_root/extracted_dir_name` and symlinked to `app_root/default`\n\n## Actions\n\n- `:install`: extracts the tarball and makes necessary symlinks\n- `:remove`: removes the tarball and run update-alternatives for all\n symlinked `bin_cmds`\n\n## Attribute Parameters\n\n- `url`: path to tarball, .tar.gz, .bin (oracle-specific), and .zip\n currently supported\n- `checksum`: SHA256 checksum, not used for security but avoid\n redownloading the archive on each chef-client run\n- `app_home`: the default for installations of this type of\n application, for example, `/usr/lib/tomcat/default`. If your\n application is not set to the default, it will be placed at the same\n level in the directory hierarchy but the directory name will be\n `app_root/extracted_directory_name + \"_alt\"`\n- `app_home_mode`: file mode for app_home, is an integer\n- `bin_cmds`: array of binary commands that should be symlinked to\n `/usr/bin`, examples are mvn, java, javac, etc. These cmds must be in\n the `bin` subdirectory of the extracted folder. Will be ignored if this\n `java_ark` is not the default\n- `owner`: owner of extracted directory, set to \"root\" by default\n- `default`: whether this the default installation of this package,\n boolean true or false\n\n## Examples\n\n # install jdk6 from Oracle\n java_ark \"jdk\" do\n url 'http://download.oracle.com/otn-pub/java/jdk/6u29-b11/jdk-6u29-linux-x64.bin'\n checksum 'a8603fa62045ce2164b26f7c04859cd548ffe0e33bfc979d9fa73df42e3b3365'\n app_home '/usr/local/java/default'\n bin_cmds [\"java\", \"javac\"]\n action :install\n end\n\nUsage\n=====\n\nSimply include the `java` recipe where ever you would like Java installed.\n\nTo install Oracle flavored Java override the `node['java']['install_flavor']` attribute with in role:\n\n name \"java\"\n description \"Install Oracle Java on Ubuntu\"\n default_attributes(\n \"java\" => {\n \"install_flavor\" => \"oracle\"\n }\n )\n run_list(\n \"recipe[java]\"\n )\n\nTo install IBM flavored Java, set the required attributes:\n\n name \"java\"\n description \"Install IBM Java on Ubuntu\"\n default_attributes(\n \"java\" => {\n \"install_flavor\" => \"ibm\",\n \"ibm\" => {\n \"accept_ibm_download_terms\" => true,\n \"url\" => \"http://fileserver.example.com/ibm-java-x86_64-sdk-7.0-4.1.bin\",\n \"checksum\" => \"The SHA256 checksum of the bin\"\n }\n }\n )\n run_list(\n \"recipe[java]\"\n )\n\n\nDevelopment\n===========\n\nThis cookbook uses\n[test-kitchen](https://github.com/opscode/test-kitchen) for\nintegration tests and\n[ChefSpec/RSpec](https://github.com/acrmp/chefspec) for unit tests.\nPull requests should pass existing tests in\n`files/default/tests/minitest-handler`.\n\nAt this time due to licensing concerns, the IBM recipe is not set up\nin test kitchen. If you would like to test this locally, copy\n.kitchen.yml to .kitchen.local.yml and add the following suite:\n\n suites:\n - name: ibm\n run_list: [\"recipe[java]\"]\n attributes:\n java:\n install_flavor: \"ibm\"\n ibm:\n accept_ibm_download_terms: true\n url: \"http://jenkins/ibm-java-x86_64-sdk-7.0-4.1.bin\"\n checksum: the-sha256-checksum\n\nLog into the IBM DeveloperWorks site to download a copy of the IBM\nJava SDK you wish to use/test, host it on an internal HTTP server, and\ncalculate the SHA256 checksum to use in the suite.\n\nLicense and Author\n==================\n\n* Author: Seth Chisamore ()\n* Author: Bryan W. Berry ()\n* Author: Joshua Timberman ()\n\nCopyright: 2008-2013, Opscode, Inc\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n", + "maintainer": "Opscode, Inc.", + "maintainer_email": "cookbooks@opscode.com", + "license": "Apache 2.0", + "platforms": { + "debian": ">= 0.0.0", + "ubuntu": ">= 0.0.0", + "centos": ">= 0.0.0", + "redhat": ">= 0.0.0", + "scientific": ">= 0.0.0", + "fedora": ">= 0.0.0", + "amazon": ">= 0.0.0", + "arch": ">= 0.0.0", + "oracle": ">= 0.0.0", + "freebsd": ">= 0.0.0", + "windows": ">= 0.0.0", + "suse": ">= 0.0.0", + "xenserver": ">= 0.0.0", + "smartos": ">= 0.0.0" + }, + "dependencies": { + "windows": ">= 0.0.0", + "aws": ">= 0.0.0" + }, + "recommendations": { + }, + "suggestions": { + }, + "conflicting": { + }, + "providing": { + }, + "replacing": { + }, + "attributes": { + }, + "groupings": { + }, + "recipes": { + "java": "Installs Java runtime", + "java::openjdk": "Installs the OpenJDK flavor of Java", + "java::oracle": "Installs the Oracle flavor of Java", + "java::oracle_i386": "Installs the 32-bit jvm without setting it as the default", + "java::oracle_rpm": "Installs the Oracle RPM flavor of Java" + } +} \ No newline at end of file diff --git a/java/providers/ark.rb b/java/providers/ark.rb new file mode 100644 index 0000000..db0b3b1 --- /dev/null +++ b/java/providers/ark.rb @@ -0,0 +1,263 @@ +# +# Author:: Bryan W. Berry () +# Cookbook Name:: java +# Provider:: ark +# +# Copyright 2011, Bryan w. Berry +# +# 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. + +require 'chef/mixin/shell_out' +include Chef::Mixin::ShellOut + +def whyrun_supported? + true +end + +def parse_app_dir_name url + file_name = url.split('/')[-1] + # funky logic to parse oracle's non-standard naming convention + # for jdk1.6 + if file_name =~ /^(jre|jdk).*$/ + major_num = file_name.scan(/\d/)[0] + update_num = file_name.scan(/\d+/)[1] + # pad a single digit number with a zero + if update_num.length < 2 + update_num = "0" + update_num + end + package_name = file_name.scan(/[a-z]+/)[0] + app_dir_name = "#{package_name}1.#{major_num}.0_#{update_num}" + else + app_dir_name = file_name.split(/(.tgz|.tar.gz|.zip)/)[0] + app_dir_name = app_dir_name.split("-bin")[0] + end + [app_dir_name, file_name] +end + +def oracle_downloaded?(download_path, new_resource) + if ::File.exists? download_path + require 'digest' + downloaded_sha = Digest::SHA256.file(download_path).hexdigest + downloaded_sha == new_resource.checksum + else + return false + end +end + +def download_direct_from_oracle(tarball_name, new_resource) + download_path = "#{Chef::Config[:file_cache_path]}/#{tarball_name}" + jdk_id = new_resource.url.scan(/\/([6789]u[0-9][0-9]?-b[0-9][0-9])\//)[0][0] + cookie = "oraclelicensejdk-#{jdk_id}-oth-JPR=accept-securebackup-cookie;gpw_e24=http://edelivery.oracle.com" + if node['java']['oracle']['accept_oracle_download_terms'] + # install the curl package + p = package "curl" do + action :nothing + end + # no converge_by block since the package provider will take care of this run_action + p.run_action(:install) + description = "download oracle tarball straight from the server" + converge_by(description) do + Chef::Log.debug "downloading oracle tarball straight from the source" + cmd = shell_out!( + %Q[ curl -L --cookie "#{cookie}" #{new_resource.url} -o #{download_path} ] + ) + end + else + Chef::Application.fatal!("You must set the attribute node['java']['oracle']['accept_oracle_download_terms'] to true if you want to download directly from the oracle site!") + end +end + +action :install do + app_dir_name, tarball_name = parse_app_dir_name(new_resource.url) + app_root = new_resource.app_home.split('/')[0..-2].join('/') + app_dir = app_root + '/' + app_dir_name + + unless new_resource.default + Chef::Log.debug("processing alternate jdk") + app_dir = app_dir + "_alt" + app_home = new_resource.app_home + "_alt" + else + app_home = new_resource.app_home + end + + unless ::File.exists?(app_dir) + Chef::Log.info "Adding #{new_resource.name} to #{app_dir}" + require 'fileutils' + + unless ::File.exists?(app_root) + description = "create dir #{app_root} and change owner to #{new_resource.owner}" + converge_by(description) do + FileUtils.mkdir app_root, :mode => new_resource.app_home_mode + FileUtils.chown new_resource.owner, new_resource.owner, app_root + end + end + + if new_resource.url =~ /^http:\/\/download.oracle.com.*$/ + download_path = "#{Chef::Config[:file_cache_path]}/#{tarball_name}" + if oracle_downloaded?(download_path, new_resource) + Chef::Log.debug("oracle tarball already downloaded, not downloading again") + else + download_direct_from_oracle tarball_name, new_resource + end + else + Chef::Log.debug("downloading tarball from an unofficial repository") + r = remote_file "#{Chef::Config[:file_cache_path]}/#{tarball_name}" do + source new_resource.url + checksum new_resource.checksum + mode 0755 + action :nothing + end + #no converge by on run_action remote_file takes care of it. + r.run_action(:create_if_missing) + end + + require 'tmpdir' + + description = "create tmpdir, extract compressed data into tmpdir, + move extracted data to #{app_dir} and delete tmpdir" + converge_by(description) do + tmpdir = Dir.mktmpdir + case tarball_name + when /^.*\.bin/ + cmd = shell_out( + %Q[ cd "#{tmpdir}"; + cp "#{Chef::Config[:file_cache_path]}/#{tarball_name}" . ; + bash ./#{tarball_name} -noregister + ] ) + unless cmd.exitstatus == 0 + Chef::Application.fatal!("Failed to extract file #{tarball_name}!") + end + when /^.*\.zip/ + cmd = shell_out( + %Q[ unzip "#{Chef::Config[:file_cache_path]}/#{tarball_name}" -d "#{tmpdir}" ] + ) + unless cmd.exitstatus == 0 + Chef::Application.fatal!("Failed to extract file #{tarball_name}!") + end + when /^.*\.(tar.gz|tgz)/ + cmd = shell_out( + %Q[ tar xvzf "#{Chef::Config[:file_cache_path]}/#{tarball_name}" -C "#{tmpdir}" ] + ) + unless cmd.exitstatus == 0 + Chef::Application.fatal!("Failed to extract file #{tarball_name}!") + end + end + + cmd = shell_out( + %Q[ mv "#{tmpdir}/#{app_dir_name}" "#{app_dir}" ] + ) + unless cmd.exitstatus == 0 + Chef::Application.fatal!(%Q[ Command \' mv "#{tmpdir}/#{app_dir_name}" "#{app_dir}" \' failed ]) + end + FileUtils.rm_r tmpdir + end + new_resource.updated_by_last_action(true) + end + + #set up .jinfo file for update-java-alternatives + java_name = app_home.split('/')[-1] + jinfo_file = "#{app_root}/.#{java_name}.jinfo" + if platform_family?("debian") && !::File.exists?(jinfo_file) + description = "Add #{jinfo_file} for debian" + converge_by(description) do + Chef::Log.debug "Adding #{jinfo_file} for debian" + template jinfo_file do + source "oracle.jinfo.erb" + variables( + :priority => new_resource.alternatives_priority, + :bin_cmds => new_resource.bin_cmds, + :name => java_name, + :app_dir => app_home + ) + action :create + end + end + new_resource.updated_by_last_action(true) + end + + #link app_home to app_dir + Chef::Log.debug "app_home is #{app_home} and app_dir is #{app_dir}" + current_link = ::File.symlink?(app_home) ? ::File.readlink(app_home) : nil + if current_link != app_dir + description = "Symlink #{app_dir} to #{app_home}" + converge_by(description) do + Chef::Log.debug "Symlinking #{app_dir} to #{app_home}" + FileUtils.rm_f app_home + FileUtils.ln_sf app_dir, app_home + end + end + + #update-alternatives + if new_resource.bin_cmds + new_resource.bin_cmds.each do |cmd| + + bin_path = "/usr/bin/#{cmd}" + alt_path = "#{app_home}/bin/#{cmd}" + priority = new_resource.alternatives_priority + + # install the alternative if needed + alternative_exists = shell_out("update-alternatives --display #{cmd} | grep #{alt_path}").exitstatus == 0 + unless alternative_exists + description = "Add alternative for #{cmd}" + converge_by(description) do + Chef::Log.debug "Adding alternative for #{cmd}" + install_cmd = shell_out("update-alternatives --install #{bin_path} #{cmd} #{alt_path} #{priority}") + unless install_cmd.exitstatus == 0 + Chef::Application.fatal!(%Q[ set alternative failed ]) + end + end + new_resource.updated_by_last_action(true) + end + + # set the alternative if default + if new_resource.default + alternative_is_set = shell_out("update-alternatives --display #{cmd} | grep \"link currently points to #{alt_path}\"").exitstatus == 0 + unless alternative_is_set + description = "Set alternative for #{cmd}" + converge_by(description) do + Chef::Log.debug "Setting alternative for #{cmd}" + set_cmd = shell_out("update-alternatives --set #{cmd} #{alt_path}").run_command + unless set_cmd.exitstatus == 0 + Chef::Application.fatal!(%Q[ set alternative failed ]) + end + end + new_resource.updated_by_last_action(true) + end + end + end + end +end + +action :remove do + app_dir_name, tarball_name = parse_app_dir_name(new_resource.url) + app_root = new_resource.app_home.split('/')[0..-2].join('/') + app_dir = app_root + '/' + app_dir_name + + if ::File.exists?(app_dir) + new_resource.bin_cmds.each do |cmd| + cmd = execute "update_alternatives" do + command "update-alternatives --remove #{cmd} #{app_dir} " + returns [0,2] + action :nothing + end + # the execute resource will take care of of the run_action(:run) + cmd.run_action(:run) + end + description = "remove #{new_resource.name} at #{app_dir}" + converge_by(description) do + Chef::Log.info "Removing #{new_resource.name} at #{app_dir}" + FileUtils.rm_rf app_dir + end + new_resource.updated_by_last_action(true) + end +end diff --git a/java/recipes/default.rb b/java/recipes/default.rb new file mode 100644 index 0000000..574aecd --- /dev/null +++ b/java/recipes/default.rb @@ -0,0 +1,29 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: java +# Recipe:: default +# +# Copyright 2008-2011, Opscode, Inc. +# +# 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. +# + +include_recipe "java::#{node['java']['install_flavor']}" + +# Purge the deprecated Sun Java packages if remove_deprecated_packages is true +%w[sun-java6-jdk sun-java6-bin sun-java6-jre].each do |pkg| + package pkg do + action :purge + only_if { node['java']['remove_deprecated_packages'] } + end +end diff --git a/java/recipes/ibm.rb b/java/recipes/ibm.rb new file mode 100644 index 0000000..ea1b271 --- /dev/null +++ b/java/recipes/ibm.rb @@ -0,0 +1,55 @@ +# Author:: Joshua Timberman () +# Cookbook Name:: java +# Recipe:: ibm +# +# Copyright 2013, Opscode, Inc. +# +# 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. + +require 'uri' +source_url = node['java']['ibm']['url'] +jdk_uri = ::URI.parse(source_url) +jdk_filename = ::File.basename(jdk_uri.path) + +unless valid_ibm_jdk_uri?(source_url) + raise "You must set the attribute `node['java']['ibm']['url']` to a valid HTTP URI" +end + +template "#{Chef::Config[:file_cache_path]}/installer.properties" do + source "ibm_jdk.installer.properties.erb" + only_if { node['java']['ibm']['accept_ibm_download_terms'] } +end + +remote_file "#{Chef::Config[:file_cache_path]}/#{jdk_filename}" do + source source_url + mode 00755 + if node['java']['ibm']['checksum'] + checksum node['java']['ibm']['checksum'] + action :create + else + action :create_if_missing + end + notifies :run, "execute[install-ibm-java]", :immediately +end + +execute "install-ibm-java" do + cwd Chef::Config[:file_cache_path] + environment({ + "_JAVA_OPTIONS" => "-Dlax.debug.level=3 -Dlax.debug.all=true", + "LAX_DEBUG" => "1" + }) + command "./#{jdk_filename} -f ./installer.properties -i silent" + creates "#{node['java']['java_home']}/jre/bin/java" +end + +include_recipe "java::set_java_home" diff --git a/java/recipes/openjdk.rb b/java/recipes/openjdk.rb new file mode 100644 index 0000000..8e47d3f --- /dev/null +++ b/java/recipes/openjdk.rb @@ -0,0 +1,50 @@ +# Author:: Bryan W. Berry () +# Author:: Seth Chisamore () +# Author:: Joshua Timberman () +# +# Cookbook Name:: java +# Recipe:: openjdk +# +# Copyright 2010-2013, Opscode, Inc. +# +# 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. + +java_location = Opscode::OpenJDK.new(node).java_location + +if platform_requires_license_acceptance? + file "/opt/local/.dlj_license_accepted" do + owner "root" + group "root" + mode "0400" + action :create + only_if { node['java']['accept_license_agreement'] } + end +end + +node['java']['openjdk_packages'].each do |pkg| + package pkg +end + +if platform_family?('debian', 'rhel', 'fedora') + bash 'update-java-alternatives' do + code <<-EOH.gsub(/^\s+/, '') + update-alternatives --install /usr/bin/java java #{java_location} 1061 && \ + update-alternatives --set java #{java_location} + EOH + only_if "update-alternatives --display java | grep '#{java_location} - priority 1061'" + end +end + +# We must include this recipe AFTER updating the alternatives or else JAVA_HOME +# will not point to the correct java. +include_recipe 'java::set_java_home' diff --git a/java/recipes/oracle.rb b/java/recipes/oracle.rb new file mode 100644 index 0000000..3e19992 --- /dev/null +++ b/java/recipes/oracle.rb @@ -0,0 +1,55 @@ +# +# Author:: Bryan W. Berry () +# Cookbook Name:: java +# Recipe:: oracle +# +# Copyright 2011, Bryan w. Berry +# +# 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. + + +java_home = node['java']["java_home"] +arch = node['java']['arch'] +jdk_version = node['java']['jdk_version'] + +#convert version number to a string if it isn't already +if jdk_version.instance_of? Fixnum + jdk_version = jdk_version.to_s +end + +case jdk_version +when "6" + tarball_url = node['java']['jdk']['6'][arch]['url'] + tarball_checksum = node['java']['jdk']['6'][arch]['checksum'] + bin_cmds = node['java']['jdk']['6']['bin_cmds'] +when "7" + tarball_url = node['java']['jdk']['7'][arch]['url'] + tarball_checksum = node['java']['jdk']['7'][arch]['checksum'] + bin_cmds = node['java']['jdk']['7']['bin_cmds'] +end + +if tarball_url =~ /example.com/ + Chef::Application.fatal!("You must change the download link to your private repository. You can no longer download java directly from http://download.oracle.com without a web broswer") +end + +include_recipe "java::set_java_home" + +java_ark "jdk" do + url tarball_url + checksum tarball_checksum + app_home java_home + bin_cmds bin_cmds + alternatives_priority 1062 + action :install +end + diff --git a/java/recipes/oracle_i386.rb b/java/recipes/oracle_i386.rb new file mode 100644 index 0000000..7472572 --- /dev/null +++ b/java/recipes/oracle_i386.rb @@ -0,0 +1,47 @@ +# +# Author:: Bryan W. Berry () +# Cookbook Name:: java +# Recipe:: oracle_i386 +# +# Copyright 2010-2011, Opscode, Inc. +# +# 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. + +java_home = node['java']["java_home"] + +case node['java']['jdk_version'] +when "6" + tarball_url = node['java']['jdk']['6']['i586']['url'] + tarball_checksum = node['java']['jdk']['6']['i586']['checksum'] + bin_cmds = node['java']['jdk']['6']['bin_cmds'] +when "7" + tarball_url = node['java']['jdk']['7']['i586']['url'] + tarball_checksum = node['java']['jdk']['7']['i586']['checksum'] + bin_cmds = node['java']['jdk']['7']['bin_cmds'] +end + +include_recipe "java::set_java_home" + +yum_package "glibc" do + arch "i686" + only_if { platform_family?( "rhel", "fedora" ) } +end + +java_ark "jdk-alt" do + url tarball_url + checksum tarball_checksum + app_home java_home + bin_cmds bin_cmds + action :install + default false +end diff --git a/java/recipes/oracle_rpm.rb b/java/recipes/oracle_rpm.rb new file mode 100644 index 0000000..033cc4d --- /dev/null +++ b/java/recipes/oracle_rpm.rb @@ -0,0 +1,56 @@ +# Author:: Christophe Arguel () +# +# Cookbook Name:: java +# Recipe:: oracle_rpm +# +# Copyright 2013, Christophe Arguel +# +# 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. + +include_recipe 'java::set_java_home' + + +slave_cmds = case node['java']['oracle_rpm']['type'] + when 'jdk' + %W[appletviewer apt ControlPanel extcheck idlj jar jarsigner javac javadoc javafxpackager javah javap java-rmi.cgi javaws jcmd jconsole jcontrol jdb jhat jinfo jmap jps jrunscript jsadebugd jstack jstat jstatd jvisualvm keytool native2ascii orbd pack200 policytool rmic rmid rmiregistry schemagen serialver servertool tnameserv unpack200 wsgen wsimport xjc] + + when 'jre' + %W[ControlPanel java_vm javaws jcontrol keytool orbd pack200 policytool rmid rmiregistry servertool tnameserv unpack200] + + else + Chef::Application.fatal "Unsupported oracle RPM type (#{node['java']['oracle_rpm']['type']})" + end + +if platform_family?('rhel', 'fedora') + + bash 'update-java-alternatives' do + java_home = node['java']['java_home'] + java_location = File.join(java_home, "bin", "java") + slave_lines = slave_cmds.inject("") do |slaves, cmd| + slaves << "--slave /usr/bin/#{cmd} #{cmd} #{File.join(java_home, "bin", cmd)} \\\n" + end + + code <<-EOH.gsub(/^\s+/, '') + update-alternatives --install /usr/bin/java java #{java_location} 1061 \ + #{slave_lines} && \ + update-alternatives --set java #{java_location} + EOH + action :nothing + end + +end + +package node['java']['oracle_rpm']['type'] do + action :upgrade + notifies :run, 'bash[update-java-alternatives]', :immediately if platform_family?('rhel', 'fedora') +end diff --git a/java/recipes/set_java_home.rb b/java/recipes/set_java_home.rb new file mode 100644 index 0000000..f695d5a --- /dev/null +++ b/java/recipes/set_java_home.rb @@ -0,0 +1,33 @@ +# Author:: Joshua Timberman () +# Cookbook Name:: java +# Recipe:: set_java_home +# +# Copyright 2013, Opscode, Inc. +# +# 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. + +ruby_block "set-env-java-home" do + block do + ENV["JAVA_HOME"] = node['java']['java_home'] + end + not_if { ENV["JAVA_HOME"] == node['java']['java_home'] } +end + +directory "/etc/profile.d" do + mode 00755 +end + +file "/etc/profile.d/jdk.sh" do + content "export JAVA_HOME=#{node['java']['java_home']}" + mode 00755 +end diff --git a/java/recipes/windows.rb b/java/recipes/windows.rb new file mode 100644 index 0000000..d31d57c --- /dev/null +++ b/java/recipes/windows.rb @@ -0,0 +1,74 @@ +# +# Author:: Kendrick Martin () +# Cookbook Name:: java +# Recipe:: windows +# +# Copyright 2008-2012 Webtrends, Inc. +# +# 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. +# + +require 'uri' + +Chef::Log.fatal("No download url set for java installer.") unless node['java'] && node['java']['windows'] && node['java']['windows']['url'] + +pkg_checksum = node['java']['windows']['checksum'] +aws_access_key_id = node['java']['windows']['aws_access_key_id'] +aws_secret_access_key = node['java']['windows']['aws_secret_access_key'] + +uri = ::URI.parse(::URI.unescape(node['java']['windows']['url'])) +cache_file_path = File.join(Chef::Config[:file_cache_path], File.basename(uri.path)) + +if aws_access_key_id && aws_secret_access_key + include_recipe 'aws::default' # install right_aws gem for aws_s3_file + + aws_s3_file cache_file_path do + aws_access_key_id aws_access_key_id + aws_secret_access_key aws_secret_access_key + checksum pkg_checksum if pkg_checksum + bucket node['java']['windows']['bucket'] + remote_path node['java']['windows']['remote_path'] + backup false + action :create + end +else + remote_file cache_file_path do + checksum pkg_checksum if pkg_checksum + source node['java']['windows']['url'] + backup false + action :create + end +end + +if node['java'].attribute?("java_home") + java_home_win = win_friendly_path(node['java']['java_home']) + additional_options = "INSTALLDIR=\"#{java_home_win}\"" + + env "JAVA_HOME" do + value java_home_win + end + + # update path + windows_path "#{java_home_win}\\bin" do + action :add + end +end + + +windows_package node['java']['windows']['package_name'] do + source cache_file_path + checksum node['java']['windows']['checksum'] + action :install + installer_type :custom + options "/s #{additional_options}" +end diff --git a/java/resources/ark.rb b/java/resources/ark.rb new file mode 100644 index 0000000..70bfee4 --- /dev/null +++ b/java/resources/ark.rb @@ -0,0 +1,38 @@ +# +# Author:: Bryan W. Berry () +# Cookbook Name:: java +# Resource:: ark +# +# Copyright 2011, Bryan w. Berry +# +# 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. + +actions :install, :remove + +attribute :url, :regex => /^https?:\/\/.*(tar.gz|tgz|bin|zip)$/, :default => nil +attribute :mirrorlist, :kind_of => Array, :default => nil +attribute :checksum, :regex => /^[a-zA-Z0-9]{40,64}$/, :default => nil +attribute :app_home, :kind_of => String, :default => nil +attribute :app_home_mode, :kind_of => Integer, :default => 0755 +attribute :bin_cmds, :kind_of => Array, :default => nil +attribute :owner, :default => "root" +attribute :default, :equal_to => [true, false], :default => true +attribute :alternatives_priority, :kind_of => Integer, :default => 1 + +# we have to set default for the supports attribute +# in initializer since it is a 'reserved' attribute name +def initialize(*args) + super + @action = :install + @supports = {:report => true, :exception => true} +end diff --git a/java/templates/default/ibm_jdk.installer.properties.erb b/java/templates/default/ibm_jdk.installer.properties.erb new file mode 100644 index 0000000..ee1fbc6 --- /dev/null +++ b/java/templates/default/ibm_jdk.installer.properties.erb @@ -0,0 +1,3 @@ +INSTALLER_UI=silent +USER_INSTALL_DIR=<%= node['java']['java_home'] %> +-fileOverwrite_<%= node['java']['java_home'] %>_uninstall/uninstall.lax=Yes diff --git a/java/templates/default/oracle.jinfo.erb b/java/templates/default/oracle.jinfo.erb new file mode 100644 index 0000000..0d10b51 --- /dev/null +++ b/java/templates/default/oracle.jinfo.erb @@ -0,0 +1,6 @@ +name=<%= @name %> +priority=<%= @priority %> +section=main + +<% @bin_cmds.each do |cmd| -%>jdk <%= cmd %> <%= @app_dir %>/bin/<%= cmd %> +<% end -%> diff --git a/play2/Gemfile b/play2/Gemfile new file mode 100644 index 0000000..3017623 --- /dev/null +++ b/play2/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'berkshelf' diff --git a/play2/LICENSE b/play2/LICENSE new file mode 100644 index 0000000..06486fe --- /dev/null +++ b/play2/LICENSE @@ -0,0 +1,12 @@ +Copyright 2012-2013 Originate, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You +may not use this file except in compliance with the License. A copy of +the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file 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. \ No newline at end of file diff --git a/play2/README.md b/play2/README.md new file mode 100644 index 0000000..682d803 --- /dev/null +++ b/play2/README.md @@ -0,0 +1,93 @@ +# play2 cookbook + +This cookbook is a working prototype allowing the deployment of applications written with +the [Play framework](http://www.playframework.com/) on Amazon's [Opsworks](http://aws.amazon.com/opsworks/). + +# Requirements + +This cookbook depends on two other cookbooks: +- [java](http://community.opscode.com/cookbooks/java) +- [artifact](http://community.opscode.com/cookbooks/artifact) + +Unfortunately, Opsworks doesn't know how to find these, so our approach is to use [Berkshelf]() to install all +the dependencies into an app-specific repository, and use that as the custom cookbook repository in Opsworks. + +Example: +```bash +git clone git://github.com/Originate/cookbooks.git originate-cookbooks +mkdir /tmp/cookbooks +# berks install will remove everything, including .git, from the target directory +berks install -b originate-cookbooks/play2/Berksfile -p /tmp/cookbooks +mv /tmp/cookbooks/* +``` + +# Usage +1. Create a new Stack, add your `custom-app-repo` as the source of custom cookbooks +2. Create a new custom layer: + - Add `play2::setup` to the setup lifecyle event recipes + - Add `play2::deploy` to the deploy lifecyle event recipes + - Start a new instance +3. Add your play application to the stack, and deploy it + +Optionally: +- If your application is not at the root of your repository, you can add custom JSON to the stack to let + this cookbook know: + + ```json + { + "deploy": { + "": { + "scm": { + "app_dir": "" + } + } + } + } + ``` +- If you want to customize some of the cookbook's attributes, you can add custom JSON to the stack: + + ```json + { + "play2": { + "version": "2.1.3", + "conf": { + "application": { + "langs": "en" + }, + "logger": { + "root": "ERROR", + "play": "INFO", + "application": "DEBUG" + } + } + } + } + ``` + +That's it, you should be good to go. + +# Attributes + +## Framework installation +|Key|Type|Description|Default| +|---|----|-----------|-------| +|`version`|`String`|The Play version to install|`2.2.0`| +|`url`|`String`|Base url to download the play distribution|`http://downloads.typesafe.com/play`| + +## Application command line options +|Key|Type|Description|Default| +|---|----|-----------|-------| +|`http_port`|`Integer`|`-Dhttp.port=...`|`nil`| +|`https_port`|`Integer`|`-Dhttps.port=...`|`nil`| +|`app_conf_file`|`String`|`-Dconfig.file=...`|`nil`| +|`log_conf_file`|`String`|`-Dlogger.file=...`|`nil`| +|`options`|`String`|Additional options that you'd like to pass to play on startup
(ie. `-Xms2048M -Xmx6144M ...`)|`nil`| +These options are only added if the attribute is defined. + +## Application configuration +|Key|Type|Description|Default| +|---|----|-----------|-------| +|`conf`|`JSON`|A JSON reprensentation of `application.conf`|`nil`| + +Enables specifying the entire application configuration from Opsworks JSON. This overwrites `conf/application.conf` +if defined. A sample configuration can be seen in the `Vagrantfile`. diff --git a/play2/Thorfile b/play2/Thorfile new file mode 100644 index 0000000..cb1aeae --- /dev/null +++ b/play2/Thorfile @@ -0,0 +1,5 @@ +# encoding: utf-8 + +require 'bundler' +require 'bundler/setup' +require 'berkshelf/thor' diff --git a/play2/attributes/default.rb b/play2/attributes/default.rb new file mode 100644 index 0000000..70cfaf9 --- /dev/null +++ b/play2/attributes/default.rb @@ -0,0 +1,2 @@ +include_attribute 'play2::play2' +include_attribute 'play2::logrotate' \ No newline at end of file diff --git a/play2/attributes/logrotate.rb b/play2/attributes/logrotate.rb new file mode 100644 index 0000000..35be044 --- /dev/null +++ b/play2/attributes/logrotate.rb @@ -0,0 +1,2 @@ +default[:logrotate][:rotate] = 30 +default[:logrotate][:dateformat] = false # set to '-%Y%m%d' to have date formatted logs \ No newline at end of file diff --git a/play2/attributes/play2.rb b/play2/attributes/play2.rb new file mode 100644 index 0000000..5561184 --- /dev/null +++ b/play2/attributes/play2.rb @@ -0,0 +1,5 @@ +default[:play2][:version] = "2.2.0" +default[:play2][:url] = "http://downloads.typesafe.com/play" + +default[:play2][:conf] = nil +default[:play2][:options] = nil \ No newline at end of file diff --git a/play2/chefignore b/play2/chefignore new file mode 100644 index 0000000..a6de142 --- /dev/null +++ b/play2/chefignore @@ -0,0 +1,96 @@ +# Put files/directories that should be ignored in this file when uploading +# or sharing to the community site. +# Lines that start with '# ' are comments. + +# OS generated files # +###################### +.DS_Store +Icon? +nohup.out +ehthumbs.db +Thumbs.db + +# SASS # +######## +.sass-cache + +# EDITORS # +########### +\#* +.#* +*~ +*.sw[a-z] +*.bak +REVISION +TAGS* +tmtags +*_flymake.* +*_flymake +*.tmproj +.project +.settings +mkmf.log + +## COMPILED ## +############## +a.out +*.o +*.pyc +*.so +*.com +*.class +*.dll +*.exe +*/rdoc/ + +# Testing # +########### +.watchr +.rspec +spec/* +spec/fixtures/* +test/* +features/* +Guardfile +Procfile + +# SCM # +####### +.git +*/.git +.gitignore +.gitmodules +.gitconfig +.gitattributes +.svn +*/.bzr/* +*/.hg/* +*/.svn/* + +# Berkshelf # +############# +Berksfile +Berksfile.lock +cookbooks/* +tmp + +# Cookbooks # +############# +CONTRIBUTING +CHANGELOG* + +# Strainer # +############ +Colanderfile +Strainerfile +.colander +.strainer + +# Vagrant # +########### +.vagrant +Vagrantfile + +# Travis # +########## +.travis.yml diff --git a/play2/definitions/opsworks_play2.rb b/play2/definitions/opsworks_play2.rb new file mode 100644 index 0000000..6dd5254 --- /dev/null +++ b/play2/definitions/opsworks_play2.rb @@ -0,0 +1,151 @@ +define :opsworks_play2 do + application = params[:app] + deploy = params[:deploy_data] + + app_dir = File.expand_path(File.join(deploy[:deploy_to], "current", deploy[:scm][:app_dir] || '.')) + shared_dir = File.join(deploy[:deploy_to], "shared") + + # Create deploy user and group if needed + group deploy[:group] + + user deploy[:user] do + action :create + comment "deploy user for #{application}" + gid deploy[:group] + + not_if do + existing_usernames = [] + Etc.passwd {|user| existing_usernames << user['name']} + existing_usernames.include?(deploy[:user]) + end + end + + if deploy[:scm][:ssh_key] != nil + directory "/home/#{deploy[:user]}/.ssh" do + recursive true + owner deploy[:user] + action :create + end + + file "/home/#{deploy[:user]}/.ssh/id_deploy" do + owner deploy[:user] + mode 0400 + content deploy[:scm][:ssh_key] + action :create_if_missing + end + + template "/home/#{deploy[:user]}/.ssh/wrap-ssh4git.sh" do + source "wrap-ssh4git.sh" + cookbook "play2" + owner deploy[:user] + mode 0700 + variables({ + :private_key => "/home/#{deploy[:user]}/.ssh/id_deploy" + }) + action :create_if_missing + end + end + + # Seems to be needed or else deploy_revision crashes + directory shared_dir do + recursive true + owner deploy[:user] + group deploy[:group] + action :create + end + + timestamped_deploy "#{deploy[:deploy_to]}" do + repo deploy[:scm][:repository] + revision deploy[:scm][:revision] || "master" + ssh_wrapper deploy[:scm][:ssh_key] != nil ? "/home/#{deploy[:user]}/.ssh/wrap-ssh4git.sh" : nil + + user deploy[:user] + group deploy[:group] + + symlink_before_migrate.clear + purge_before_symlink(%w{logs}) + create_dirs_before_symlink.clear + symlinks({"logs" => "#{deploy[:scm][:app_dir]}/logs"}) + + before_symlink do + directory ::File.join(shared_dir, "logs") do + action :create + end + end + + # restart_command "echo whoami && sudo service #{application} restart" + before_restart do + # Create the application configuration file + template ::File.join(app_dir, "conf/application.conf") do + source "app_conf.erb" + cookbook "play2" + owner deploy[:user] + group deploy[:group] + mode "0644" + backup false + variables({ + :flat_conf => play_flat_config(node[:play2][:conf] || {}) + }) + only_if do + node[:play2][:conf] != nil + end + end + + # Create the logging configuration file + template ::File.join(app_dir, "conf/logger.xml") do + source "app_logging.erb" + cookbook "play2" + owner deploy[:user] + group deploy[:group] + mode "0644" + backup false + end + + template "/etc/logrotate.d/opsworks_#{application}" do + source "app_logrotate.erb" + cookbook "play2" + owner "root" + group "root" + mode "0644" + backup false + variables( :log_dirs => [ ::File.join(shared_dir, "logs") ] ) + end + + execute "package #{application}" do + cwd app_dir + user "root" + command "play clean stage" + end + + # Create the service for the application + template "/etc/init.d/#{application}" do + source "app_initd.erb" + cookbook "play2" + owner "root" + group "root" + mode "0755" + backup false + variables({ + :name => application, + :path => app_dir, + :deploy_to => deploy[:deploy_to], + :options => play_options(), + :command => "target/start", + :url => play_url() + }) + end + + service application do + supports :status => true, :start => true, :stop => true, :restart => true + action :enable + end + end + + action :deploy + end + + execute "restart #{application}" do + user "root" + command "sudo service #{application} restart" + end +end \ No newline at end of file diff --git a/play2/libraries/play2.rb b/play2/libraries/play2.rb new file mode 100644 index 0000000..5c9f5a4 --- /dev/null +++ b/play2/libraries/play2.rb @@ -0,0 +1,49 @@ +def play_url() + if port = node[:play2][:https_port] + protocol = "https" + else + port = node[:play2][:http_port] || 9000 + protocol = "http" + end + + return "#{protocol}://localhost:#{port.to_s}" +end + +def play_options() + return [default_play_options(), node[:play2][:options]].compact.join(" ") +end + +def default_play_options() + options = [] + + if node[:play2][:http_port] + options << "-Dhttp.port=#{node[:play2][:http_port]}" + end + if node[:play2][:https_port] + options << "-Dhttps.port=#{node[:play2][:https_port]}" + end + + if node[:play2][:app_conf_file] + options << "-Dconfig.file=#{node[:play2][:app_conf_file]}" + end + + if node[:play2][:log_conf_file] + options << "-Dlogger.file=#{node[:play2][:log_conf_file]}" + end + + return options.join(" ") +end + +def play_flat_config(config, prefix = nil) + flat_config = {} + config.each_pair do |key, val| + flat_key = [prefix, key].compact.join('.') + + if val.is_a?(Hash) + flat_config.merge!(play_flat_config(val, flat_key)) + else + flat_config[flat_key] = val + end + end + flat_config +end \ No newline at end of file diff --git a/play2/metadata.json b/play2/metadata.json new file mode 100644 index 0000000..affc979 --- /dev/null +++ b/play2/metadata.json @@ -0,0 +1 @@ +{"name":"play2","description":"Installs/Configures a Play2 application","long_description":"# play2 cookbook\n\nThis cookbook is a working prototype allowing the deployment of applications written with \nthe [Play framework](http://www.playframework.com/) on Amazon's [Opsworks](http://aws.amazon.com/opsworks/).\n\n# Requirements\n\nThis cookbook depends on two other cookbooks:\n- [java](http://community.opscode.com/cookbooks/java)\n- [artifact](http://community.opscode.com/cookbooks/artifact)\n\nUnfortunately, Opsworks doesn't know how to find these, so our approach is to use [Berkshelf]() to install all \nthe dependencies into an app-specific repository, and use that as the custom cookbook repository in Opsworks.\n\nExample:\n```bash\ngit clone git://github.com/Originate/cookbooks.git originate-cookbooks\nmkdir /tmp/cookbooks\n# berks install will remove everything, including .git, from the target directory\nberks install -b originate-cookbooks/play2/Berksfile -p /tmp/cookbooks\nmv /tmp/cookbooks/* \n```\n\n# Usage\n1. Create a new Stack, add your `custom-app-repo` as the source of custom cookbooks\n2. Create a new custom layer:\n - Add `play2::setup` to the setup lifecyle event recipes\n - Add `play2::deploy` to the deploy lifecyle event recipes\n - Start a new instance\n3. Add your play application to the stack, and deploy it\n\nOptionally:\n- If your application is not at the root of your repository, you can add custom JSON to the stack to let\n this cookbook know:\n\n ```json\n {\n \"deploy\": {\n \"\": {\n \"scm\": {\n \"app_dir\": \"\"\n }\n }\n }\n }\n ```\n- If you want to customize some of the cookbook's attributes, you can add custom JSON to the stack:\n\n ```json\n {\n \"play2\": {\n \"version\": \"2.1.3\",\n \"conf\": {\n \"application\": {\n \"langs\": \"en\"\n },\n \"logger\": {\n \"root\": \"ERROR\",\n \"play\": \"INFO\",\n \"application\": \"DEBUG\"\n }\n }\n }\n }\n ```\n\nThat's it, you should be good to go.\n\n# Attributes\n\n## Framework installation\n|Key|Type|Description|Default|\n|---|----|-----------|-------|\n|`version`|`String`|The Play version to install|`2.2.0`|\n|`url`|`String`|Base url to download the play distribution|`http://downloads.typesafe.com/play`|\n\n## Application command line options\n|Key|Type|Description|Default|\n|---|----|-----------|-------|\n|`http_port`|`Integer`|`-Dhttp.port=...`|`nil`|\n|`https_port`|`Integer`|`-Dhttps.port=...`|`nil`|\n|`app_conf_file`|`String`|`-Dconfig.file=...`|`nil`|\n|`log_conf_file`|`String`|`-Dlogger.file=...`|`nil`|\n|`options`|`String`|Additional options that you'd like to pass to play on startup
(ie. `-Xms2048M -Xmx6144M ...`)|`nil`|\nThese options are only added if the attribute is defined.\n\n## Application configuration\n|Key|Type|Description|Default|\n|---|----|-----------|-------|\n|`conf`|`JSON`|A JSON reprensentation of `application.conf`|`nil`|\n\nEnables specifying the entire application configuration from Opsworks JSON. This overwrites `conf/application.conf` \nif defined. A sample configuration can be seen in the `Vagrantfile`.\n","maintainer":"Maxime Bury","maintainer_email":"maxime.bury@originate.com","license":"All rights reserved","platforms":{},"dependencies":{"java":"~> 1.14.0","artifact":"~> 1.10.3"},"recommendations":{},"suggestions":{},"conflicting":{},"providing":{},"replacing":{},"attributes":{},"groupings":{},"recipes":{},"version":"0.1.0"} \ No newline at end of file diff --git a/play2/recipes/deploy.rb b/play2/recipes/deploy.rb new file mode 100644 index 0000000..34a7ebc --- /dev/null +++ b/play2/recipes/deploy.rb @@ -0,0 +1,11 @@ +# +# Cookbook Name:: play2 +# Recipe:: deploy +# + +node[:deploy].each do |application, deploy| + opsworks_play2 do + app application + deploy_data deploy + end +end \ No newline at end of file diff --git a/play2/recipes/setup.rb b/play2/recipes/setup.rb new file mode 100644 index 0000000..fdaf10a --- /dev/null +++ b/play2/recipes/setup.rb @@ -0,0 +1,30 @@ +# +# Cookbook Name:: play2 +# Recipe:: setup +# + +include_recipe "java" + +package 'git' + +url = node[:play2][:url] +version = node[:play2][:version] + +artifact_deploy "play2" do + version node[:play2][:version] + artifact_location "#{url}/#{version}/play-#{version}.zip" + + deploy_to node[:play2][:deploy_to] || "/opt/play" + shared_directories [] + + owner "root" + group "root" + + after_deploy Proc.new { + link "/usr/bin/play" do + to "#{release_path}/play-#{version}/play" + end + } + + action :nothing if Chef::Artifact.get_current_deployed_version(deploy_to) == version +end \ No newline at end of file diff --git a/play2/templates/default/app_conf.erb b/play2/templates/default/app_conf.erb new file mode 100644 index 0000000..1614684 --- /dev/null +++ b/play2/templates/default/app_conf.erb @@ -0,0 +1,3 @@ +<% @flat_conf.to_a.sort{|a,b| a[0] <=> b[0]}.each do |kv| %> +<%= kv[0] %>=<%= kv[1].inspect %> +<% end %> \ No newline at end of file diff --git a/play2/templates/default/app_initd.erb b/play2/templates/default/app_initd.erb new file mode 100644 index 0000000..acdba70 --- /dev/null +++ b/play2/templates/default/app_initd.erb @@ -0,0 +1,113 @@ +#!/bin/sh +# chkconfig: 345 20 80 +# description: <%= @name %> playframework application start/shutdown script +# processname: <%= @name %> + +# User running the Play process +USER=root + +# Name of the application +APPLICATION_NAME="<%= @name %>" +# Path to the application source +APPLICATION_PATH="<%= @path %>" +# Command line options for starting Play +APPLICATION_OPTIONS="<%= @options %>" +# URL to ping the active server +APPLICATION_URL="<%= @url %>" + +. /etc/init.d/functions + +RETVAL=0 + +[ -d $APPLICATION_PATH/logs ] || exit 1 +[ -f $APPLICATION_PATH/<%= @command %> ] || exit 1 + +[ -f $APPLICATION_PATH/logs/service.log ] || touch $APPLICATION_PATH/logs/service.log + +start() { + echo -n "Starting Play service: <%= @name %>" | tee -a $APPLICATION_PATH/logs/service.log + echo >> $APPLICATION_PATH/logs/service.log + + CHECK_UP=< /dev/null; do + echo -n "Pinging $APPLICATION_URL" | tee -a $APPLICATION_PATH/logs/service.log + curl -k $APPLICATION_URL > /dev/null 2> /dev/null + if [ $? -eq 0 ]; then + exit 0 + fi + sleep 1.5 + done + exit 1 + else + exit $RETVAL + fi +CMD + su $USER -c "exec ${APPLICATION_PATH}/<%= @command %> ${APPLICATION_OPTIONS} >> $APPLICATION_PATH/logs/service.log 2>&1 & $CHECK_UP" + + RETVAL=$? + + if [ $RETVAL -eq 0 ]; then + echo_success + else + echo_failure + fi + echo + + return $RETVAL +} + +stop() { + echo -n "Shutting down Play service: <%= @name %>" | tee -a $APPLICATION_PATH/logs/service.log + echo >> $APPLICATION_PATH/logs/service.log + + for PID in `find <%= @deploy_to %>/releases -name RUNNING_PID` + do + echo "Found $PID" >> $APPLICATION_PATH/logs/service.log + if ps -p `cat $PID` > /dev/null + then + su $USER -c "kill -9 `cat $PID`" + fi + + su $USER -c "rm -rf $PID" + done + + echo_success + echo +} + +status() { + #TODO + echo +} + +clean() { + echo "Cleaning up Play service: <%= @name %>" >> $APPLICATION_PATH/logs/service.log + rm -f ${APPLICATION_PATH}/RUNNING_PID +} + +case "$1" in + start) + clean + start + ;; + stop) + stop + ;; + restart|reload) + stop + sleep 10 + start + ;; + status) + status + ;; + clean) + clean + ;; + *) + echo "Usage: $0 {start|stop|restart|status}" +esac +exit 0 diff --git a/play2/templates/default/app_logging.erb b/play2/templates/default/app_logging.erb new file mode 100644 index 0000000..872c230 --- /dev/null +++ b/play2/templates/default/app_logging.erb @@ -0,0 +1,19 @@ + + + + + + ${application.home}/logs/application.log + + %date - [%level] - from %logger in %thread %n%message%n%xException%n + + + + + + + + + + + \ No newline at end of file diff --git a/play2/templates/default/app_logrotate.erb b/play2/templates/default/app_logrotate.erb new file mode 100644 index 0000000..6fe91e9 --- /dev/null +++ b/play2/templates/default/app_logrotate.erb @@ -0,0 +1,14 @@ +<% @log_dirs.each do |dir| %><%= dir %>/*.log <% end %> { + daily + missingok + rotate <%= node[:logrotate][:rotate] %> + compress + delaycompress + <% if node[:logrotate][:dateformat] -%> + dateext + dateformat <%= node[:logrotate][:dateformat] %> + <% end -%> + notifempty + copytruncate + sharedscripts +} \ No newline at end of file diff --git a/play2/templates/default/wrap-ssh4git.sh b/play2/templates/default/wrap-ssh4git.sh new file mode 100644 index 0000000..f6393c6 --- /dev/null +++ b/play2/templates/default/wrap-ssh4git.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +/usr/bin/env ssh -o "StrictHostKeyChecking=no" -i "<%= @private_key %>" $1 $2 diff --git a/windows/CHANGELOG.md b/windows/CHANGELOG.md new file mode 100644 index 0000000..d077520 --- /dev/null +++ b/windows/CHANGELOG.md @@ -0,0 +1,181 @@ +## Future + +* package preseeding/`response_file` support +* package installation location via a `target_dir` attribute. +* [COOK-666] `windows_package` should support CoApp packages +* WindowsRebootHandler/`windows_reboot` LWRP should support kicking off subsequent chef run on reboot. + +## v1.8.10: + +When using Windows qualified filepaths (C:/foo), the #absolute? method +for URI returns true, because "C" is the scheme. + +This change checks that the URI is http or https scheme, so it can be +passed off to remote_file appropriately. + +* [COOK-2729] - allow only http, https URI schemes + +## v1.8.8: + +* [COOK-2729] - helper should use URI rather than regex and bare string + +## v1.8.6: + +* [COOK-968] - `windows_package` provider should gracefully handle paths with spaces +* [COOK-222] - `windows_task` resource does not declare :change action +* [COOK-241] - Windows cookbook should check for redefined constants +* [COOK-248] - Windows package install type is case sensitive + +## v1.8.4: + +* [COOK-2336] - MSI That requires reboot returns with RC 3010 and + causes chef run failure +* [COOK-2368] - `version` attribute of the `windows_package` provider + should be documented + +## v1.8.2: + +**Important**: Use powershell in nodes expanded run lists to ensure + powershell is downloaded, as powershell has a dependency on this + cookbook; v1.8.0 created a circular dependency. + +* [COOK-2301] - windows 1.8.0 has circular dependency on powershell + +## v1.8.0: + +* [COOK-2126] - Add checksum attribute to `windows_zipfile` +* [COOK-2142] - Add printer and `printer_port` LWRPs +* [COOK-2149] - Chef::Log.debug Windows Package command line +* [COOK-2155] -`windows_package` does not send checksum to + `cached_file` in `installer_type` + +## v1.7.0: + +* [COOK-1745] - allow for newer versions of rubyzip + +## v1.6.0: + +* [COOK-2048] - undefined method for Falseclass on task :change when + action is :nothing (and task doesn't exist) +* [COOK-2049] - Add `windows_pagefile` resource + +## v1.5.0: + +* [COOK-1251] - Fix LWRP "NotImplementedError" +* [COOK-1921] - Task LWRP will return true for resource exists when no + other scheduled tasks exist +* [COOK-1932] - Include :change functionality to windows task lwrp + +## v1.4.0: + +* [COOK-1571] - `windows_package` resource (with msi provider) does not +accept spaces in filename +* [COOK-1581] - Windows cookbook needs a scheduled tasks LWRP +* [COOK-1584] - `windows_registry` should support all registry types + +## v1.3.4: + +* [COOK-1173] - `windows_registry` throws Win32::Registry::Error for + action :remove on a nonexistent key +* [COOK-1182] - windows package sets start window title instead of + quoting a path +* [COOK-1476] - zipfile lwrp should support :zip action +* [COOK-1485] - package resource fails to perform install correctly + when "source" contains quote +* [COOK-1519] - add action :remove for path lwrp + +## v1.3.2: + +* [COOK-1033] - remove the `libraries/ruby_19_patches.rb` file which + causes havoc on non-Windows systems. +* [COOK-811] - add a timeout parameter attribute for `windows_package` + +## v1.3.0: + +* [COOK-1323] - Update for changes in Chef 0.10.10. + - Setting file mode doesn't make sense on Windows (package provider + - and `reboot_handler` recipe) + - Prefix ::Win32 to avoid namespace collision with Chef::Win32 + - (`registry_helper` library) + - Use chef_gem instead of gem_package so gems get installed correctly + under the Ruby environment Chef runs in (reboot_handler recipe, + zipfile provider) + +## v1.2.12: + +* [COOK-1037] - specify version for rubyzip gem +* [COOK-1007] - `windows_feature` does not work to remove features with + dism +* [COOK-667] - shortcut resource + provider for Windows platforms + +## v1.2.10 + +* [COOK-939] - add `type` parameter to `windows_registry` to allow binary registry keys. +* [COOK-940] - refactor logic so multiple values get created. + +## v1.2.8 + +* FIX: Older Windows (Windows Server 2003) sometimes return 127 on successful forked commands +* FIX: `windows_package`, ensure we pass the WOW* registry redirection flags into reg.open + +## v1.2.6 + +* patch to fix [CHEF-2684], Open4 is named Open3 in Ruby 1.9 +* Ruby 1.9's Open3 returns 0 and 42 for successful commands +* retry keyword can only be used in a rescue block in Ruby 1.9 + +## v1.2.4 + +* `windows_package` - catch Win32::Registry::Error that pops up when searching certain keys + +## v1.2.2 + +* combined numerous helper libarires for easier sharing across libaries/LWRPs +* renamed Chef::Provider::WindowsFeature::Base file to the more descriptive `feature_base.rb` +* refactored `windows_path` LWRP + * :add action should MODIFY the the underlying ENV variable (vs CREATE) + * deleted greedy :remove action until it could be made more idempotent +* added a `windows_batch` resource/provider for running batch scripts remotely + +## v1.2.0 + +* [COOK-745] gracefully handle required server restarts on Windows platform + * WindowsRebootHandler for requested and pending reboots + * `windows_reboot` LWRP for requesting (receiving notifies) reboots + * `reboot_handler` recipe for enabling WindowsRebootHandler as a report handler +* [COOK-714] Correct initialize misspelling +* RegistryHelper - new `get_values` method which returns all values for a particular key. + +## v1.0.8 + +* [COOK-719] resource/provider for managing windows features +* [COOK-717] remove `windows_env_vars` resource as env resource exists in core chef +* new `Windows::Version` helper class +* refactored `Windows::Helper` mixin + +## v1.0.6 + +* added `force_modify` action to `windows_registry` resource +* add `win_friendly_path` helper +* re-purpose default recipe to install useful supporting windows related gems + +## v1.0.4 + +* [COOK-700] new resources and improvements to the `windows_registry` provider (thanks Paul Morton!) + * Open the registry in the bitednes of the OS + * Provide convenience methods to check if keys and values exit + * Provide convenience method for reading registry values + * NEW - `windows_auto_run` resource/provider + * NEW - `windows_env_vars` resource/provider + * NEW - `windows_path` resource/provider +* re-write of the `windows_package` logic for determining current installed packages +* new checksum attribute for `windows_package` resource...useful for remote packages + +## v1.0.2: + +* [COOK-647] account for Wow6432Node registry redirecter +* [COOK-656] begin/rescue on win32/registry + +## 1.0.0: + +* [COOK-612] initial release diff --git a/windows/CONTRIBUTING b/windows/CONTRIBUTING new file mode 100644 index 0000000..89ac873 --- /dev/null +++ b/windows/CONTRIBUTING @@ -0,0 +1,29 @@ +If you would like to contribute, please open a ticket in JIRA: + +* http://tickets.opscode.com + +Create the ticket in the COOK project and use the cookbook name as the +component. + +For all code contributions, we ask that contributors sign a +contributor license agreement (CLA). Instructions may be found here: + +* http://wiki.opscode.com/display/chef/How+to+Contribute + +When contributing changes to individual cookbooks, please do not +modify the version number in the metadata.rb. Also please do not +update the CHANGELOG.md for a new version. Not all changes to a +cookbook may be merged and released in the same versions. Opscode will +handle the version updates during the release process. You are welcome +to correct typos or otherwise make updates to documentation in the +README. + +If a contribution adds new platforms or platform versions, indicate +such in the body of the commit message(s), and update the relevant +COOK ticket. When writing commit messages, it is helpful for others if +you indicate the COOK ticket. For example: + + git commit -m '[COOK-1041] Updated pool resource to correctly delete.' + +In the ticket itself, it is also helpful if you include log output of +a successful Chef run, but this is not absolutely required. diff --git a/windows/LICENSE b/windows/LICENSE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/windows/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 [yyyy] [name of copyright owner] + +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/windows/README.md b/windows/README.md new file mode 100644 index 0000000..2829cbd --- /dev/null +++ b/windows/README.md @@ -0,0 +1,609 @@ +Description +=========== + +Provides a set of Windows-specific primitives (Chef resources) meant to aid in the creation of cookbooks/recipes targeting the Windows platform. + +Requirements +============ + +Version 1.3.0+ of this cookbook requires Chef 0.10.10+. + +Platform +-------- + +* Windows XP +* Windows Vista +* Windows Server 2003 R2 +* Windows 7 +* Windows Server 2008 (R1, R2) + +The `windows_task` LWRP requires Windows Server 2008 due to its API usage. + +Cookbooks +--------- + +The following cookbooks provided by Opscode are required as noted: + +* chef_handler (`windows::reboot_handler` leverages the chef_handler LWRP) +* powershell - The Printer and Printer Port LWRP require Powershell. + +**NOTE** We cannot specifically depend on Opscode's powershell, + because powershell depends on this cookbook. Ensure that + `recipe[powershell]` exists in the node's expanded run list so it + gets downloaded where the printer LWRPs are used. + +Attributes +========== + +* `node['windows']['allow_pending_reboots']` - used to configure the `WindowsRebootHandler` (via the `windows::reboot_handler` recipe) to act on pending reboots. default is true (ie act on pending reboots). The value of this attribute only has an effect if the `windows::reboot_handler` is in a node's run list. + +Resource/Provider +================= + +windows\_auto\_run +------------------ + +### Actions + +- :create: Create an item to be run at login +- :remove: Remove an item that was previously setup to run at login + +### Attribute Parameters + +- :name: Name attribute. The name of the value to be stored in the registry +- :program: The program to be run at login +- :args: The arguments for the program + +### Examples + + # Run BGInfo at login + windows_auto_run 'BGINFO' do + program "C:/Sysinternals/bginfo.exe" + args "\"C:/Sysinternals/Config.bgi\" /NOLICPROMPT /TIMER:0" + not_if { Registry.value_exists?(AUTO_RUN_KEY, 'BGINFO') } + action :create + end + + +windows\_batch +-------------- + +Execute a batch script using the cmd.exe interpreter (much like the script resources for bash, csh, powershell, perl, python and ruby). A temporary file is created and executed like other script resources, rather than run inline. By their nature, Script resources are not idempotent, as they are completely up to the user's imagination. Use the `not_if` or `only_if` meta parameters to guard the resource for idempotence. + +### Actions + +- :run: run the batch file + +### Attribute Parameters + +- command: name attribute. Name of the command to execute. +- code: quoted string of code to execute. +- creates: a file this command creates - if the file exists, the command will not be run. +- cwd: current working directory to run the command from. +- flags: command line flags to pass to the interpreter when invoking. +- user: A user name or user ID that we should change to before running this command. +- group: A group name or group ID that we should change to before running this command. + +### Examples + + windows_batch "unzip_and_move_ruby" do + code <<-EOH + 7z.exe x #{Chef::Config[:file_cache_path]}/ruby-1.8.7-p352-i386-mingw32.7z -oC:\\source -r -y + xcopy C:\\source\\ruby-1.8.7-p352-i386-mingw32 C:\\ruby /e /y + EOH + end + + windows_batch "echo some env vars" do + code <<-EOH + echo %TEMP% + echo %SYSTEMDRIVE% + echo %PATH% + echo %WINDIR% + EOH + end + +windows\_feature +---------------- + +Windows Roles and Features can be thought of as built-in operating system packages that ship with the OS. A server role is a set of software programs that, when they are installed and properly configured, lets a computer perform a specific function for multiple users or other computers within a network. A Role can have multiple Role Services that provide functionality to the Role. Role services are software programs that provide the functionality of a role. Features are software programs that, although they are not directly parts of roles, can support or augment the functionality of one or more roles, or improve the functionality of the server, regardless of which roles are installed. Collectively we refer to all of these attributes as 'features'. + +This resource allows you to manage these 'features' in an unattended, idempotent way. + +There are two providers for the `windows_features` which map into Microsoft's two major tools for managing roles/features: [Deployment Image Servicing and Management (DISM)](http://msdn.microsoft.com/en-us/library/dd371719(v=vs.85).aspx) and [Servermanagercmd](http://technet.microsoft.com/en-us/library/ee344834(WS.10).aspx) (The CLI for Server Manager). As Servermanagercmd is deprecated, Chef will set the default provider to `Chef::Provider::WindowsFeature::DISM` if DISM is present on the system being configured. The default provider will fall back to `Chef::Provider::WindowsFeature::ServerManagerCmd`. + +For more information on Roles, Role Services and Features see the [Microsoft TechNet article on the topic](http://technet.microsoft.com/en-us/library/cc754923.aspx). For a complete list of all features that are available on a node type either of the following commands at a command prompt: + + dism /online /Get-Features + servermanagercmd -query + +### Actions + +- :install: install a Windows role/feature +- :remove: remove a Windows role/feature + +### Attribute Parameters + +- feature_name: name of the feature/role to install. The same feature may have different names depending on the provider used (ie DHCPServer vs DHCP; DNS-Server-Full-Role vs DNS). + +### Providers + +- **Chef::Provider::WindowsFeature::DISM**: Uses Deployment Image Servicing and Management (DISM) to manage roles/features. +- **Chef::Provider::WindowsFeature::ServerManagerCmd**: Uses Server Manager to manage roles/features. + +### Examples + + # enable the node as a DHCP Server + windows_feature "DHCPServer" do + action :install + end + + # enable TFTP + windows_feature "TFTP" do + action :install + end + + # disable Telnet client/server + %w{ TelnetServer TelnetClient }.each do |feature| + windows_feature feature do + action :remove + end + end + +windows\_package +---------------- + +Manage Windows application packages in an unattended, idempotent way. + +The following application installers are currently supported: + +* MSI packages +* InstallShield +* Wise InstallMaster +* Inno Setup +* Nullsoft Scriptable Install System + +If the proper installer type is not passed into the resource's installer_type attribute, the provider will do it's best to identify the type by introspecting the installation package. If the installation type cannot be properly identified the `:custom` value can be passed into the installer_type attribute along with the proper flags for silent/quiet installation (using the `options` attribute..see example below). + +__PLEASE NOTE__ - For proper idempotence the resource's `package_name` should be the same as the 'DisplayName' registry value in the uninstallation data that is created during package installation. The easiest way to definitively find the proper 'DisplayName' value is to install the package on a machine and search for the uninstall information under the following registry keys: + +* `HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall` +* `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall` +* `HKEY_LOCAL_MACHINE\Software\Wow6464Node\Microsoft\Windows\CurrentVersion\Uninstall` + +For maximum flexibility the `source` attribute supports both remote and local installation packages. + +### Actions + +- :install: install a package +- :remove: remove a package. The remove action is completely hit or miss as many application uninstallers do not support a full silent/quiet mode. + +### Attribute Parameters + +- package_name: name attribute. The 'DisplayName' of the application installation package. +- source: The source of the windows installer. This can either be a URI or a local path. +- installer_type: They type of windows installation package. valid values are: :msi, :inno, :nsis, :wise, :installshield, :custom. If this value is not provided, the provider will do it's best to identify the installer type through introspection of the file. +- checksum: useful if source is remote, the SHA-256 checksum of the file--if the local file matches the checksum, Chef will not download it +- options: Additional options to pass the underlying installation command +- timeout: set a timeout for the package download (default 600 seconds) +- version: The version number of this package, as indicated by the 'DisplayVersion' value in one of the 'Uninstall' registry keys. If the given version number does equal the 'DisplayVersion' in the registry, the package will be installed. +- success_codes: set an array of possible successful installation + return codes. Previously this was hardcoded, but certain MSIs may + have a different return code, e.g. 3010 for reboot required. Must be + an array, and defaults to `[0, 42, 127]`. + +### Examples + + # install PuTTY (InnoSetup installer) + windows_package "PuTTY version 0.60" do + source "http://the.earth.li/~sgtatham/putty/latest/x86/putty-0.60-installer.exe" + installer_type :inno + action :install + end + + # install 7-Zip (MSI installer) + windows_package "7-Zip 9.20 (x64 edition)" do + source "http://downloads.sourceforge.net/sevenzip/7z920-x64.msi" + action :install + end + + # install Notepad++ (Y U No Emacs?) using a local installer + windows_package "Notepad++" do + source "c:/installation_files/npp.5.9.2.Installer.exe" + action :install + end + + # install VLC for that Xvid (NSIS installer) + windows_package "VLC media player 1.1.10" do + source "http://superb-sea2.dl.sourceforge.net/project/vlc/1.1.10/win32/vlc-1.1.10-win32.exe" + action :install + end + + # install Firefox as custom installer and manually set the silent install flags + windows_package "Mozilla Firefox 5.0 (x86 en-US)" do + source "http://archive.mozilla.org/pub/mozilla.org/mozilla.org/firefox/releases/5.0/win32/en-US/Firefox%20Setup%205.0.exe" + options "-ms" + installer_type :custom + action :install + end + + # Google Chrome FTW (MSI installer) + windows_package "Google Chrome" do + source "https://dl-ssl.google.com/tag/s/appguid%3D%7B8A69D345-D564-463C-AFF1-A69D9E530F96%7D%26iid%3D%7B806F36C0-CB54-4A84-A3F3-0CF8A86575E0%7D%26lang%3Den%26browser%3D3%26usagestats%3D0%26appname%3DGoogle%2520Chrome%26needsadmin%3Dfalse/edgedl/chrome/install/GoogleChromeStandaloneEnterprise.msi" + action :install + end + + # remove Google Chrome (but why??) + windows_package "Google Chrome" do + action :remove + end + + # remove 7-Zip + windows_package "7-Zip 9.20 (x64 edition)" do + action :remove + end + + +windows\_printer\_port +---------------------- + +**Note** Include `recipe[powershell]` on the node's expanded run list + to ensure the powershell cookbook is downloaded to avoid circular + dependency. + +Create and delete TCP/IPv4 printer ports. + +### Actions + +- :create: Create a TCIP/IPv4 printer port. This is the default action. +- :delete: Delete a TCIP/IPv4 printer port + +### Attribute Parameters + +- :ipv4_address: Name attribute. Required. IPv4 address, e.g. "10.0.24.34" +- :port_name: Port name. Optional. Defaults to "IP_" + :ipv4_address +- :port_number: Port number. Optional. Defaults to 9100. +- :port_description: Port description. Optional. +- :snmp_enabled: Boolean. Optional. Defaults to false. +- :port_protocol: Port protocol, 1 (RAW), or 2 (LPR). Optional. Defaults to 1. + +### Examples + + # simplest example. Creates a TCP/IP printer port named "IP_10.4.64.37" + # with all defaults + windows_printer_port '10.4.64.37' do + end + + # delete a printer port + windows_printer_port '10.4.64.37' do + action :delete + end + + # delete a port with a custom port_name + windows_printer_port '10.4.64.38' do + port_name "My awesome port" + action :delete + end + + # Create a port with more options + windows_printer_port '10.4.64.39' do + port_name "My awesome port" + snmp_enabled true + port_protocol 2 + end + + +windows\_printer +---------------- + +**Note** Include `recipe[powershell]` on the node's expanded run list + to ensure the powershell cookbook is downloaded to avoid circular + dependency. + +Create Windows printer. Note that this doesn't currently install a printer +driver. You must already have the driver installed on the system. + +The Windows Printer LWRP will automatically create a TCP/IP printer port for you using the `ipv4_address` property. If you want more granular control over the printer port, just create it using the `windows_printer_port` LWRP before creating the printer. + +### Actions + +- :create: Create a new printer +- :delete: Delete a new printer + +### Attribute Parameters + +- :device_id: Name attribute. Required. Printer queue name, e.g. "HP LJ 5200 in fifth floor copy room" +- :comment: Optional string describing the printer queue. +- :default: Boolean. Optional. Defaults to false. Note that Windows sets the first printer defined to the default printer regardless of this setting. +- :driver_name: String. Required. Exact name of printer driver. Note that the printer driver must already be installed on the node. +- :location: Printer location, e.g. "Fifth floor copy room", or "US/NYC/Floor42/Room4207" +- :shared: Boolean. Defaults to false. +- :share_name: Printer share name. +- :ipv4_address: Printer IPv4 address, e.g. "10.4.64.23". You don't have to be able to ping the IP addresss to set it. Required. + + +### Examples + + # create a printer + windows_printer 'HP LaserJet 5th Floor' do + driver_name 'HP LaserJet 4100 Series PCL6' + ipv4_address '10.4.64.38' + end + + # delete a printer + # Note: this doesn't delete the associated printer port. + # See `windows_printer_port` above for how to delete the port. + windows_printer 'HP LaserJet 5th Floor' do + action :delete + end + + +windows\_reboot +--------------- + +Sets required data in the node's run_state to notify `WindowsRebootHandler` a reboot is requested. If Chef run completes successfully a reboot will occur if the `WindowsRebootHandler` is properly registered as a report handler. As an action of `:request` will cause a node to reboot every Chef run, this resource is usually notified by other resources...ie restart node after a package is installed (see example below). + +### Actions + +- :request: requests a reboot at completion of successful Cher run. requires `WindowsRebootHandler` to be registered as a report handler. +- :cancel: remove reboot request from node.run_state. this will cancel *ALL* previously requested reboots as this is a binary state. + +### Attribute Parameters + +- :timeout: Name attribute. timeout delay in seconds to wait before proceeding with the requested reboot. default is 60 seconds +- :reason: comment on the reason for the reboot. default is 'Opscode Chef initiated reboot' + +### Examples + + # if the package installs, schedule a reboot at end of chef run + windows_reboot 60 do + reason 'cause chef said so' + action :nothing + end + windows_package 'some_package' do + action :install + notifies :request, 'windows_reboot[60]' + end + + # cancel the previously requested reboot + windows_reboot 60 do + action :cancel + end + +windows\_registry +----------------- + +Creates and modifies Windows registry keys. + +*Change in v1.3.0: The Win32 classes use `::Win32` to avoid namespace conflict with `Chef::Win32` (introduced in Chef 0.10.10).* + +### Actions + +- :create: create a new registry key with the provided values. +- :modify: modify an existing registry key with the provided values. +- :force_modify: modify an existing registry key with the provided values. ensures the value is actually set by checking multiple times. useful for fighting race conditions where two processes are trying to set the same registry key. This will be updated in the near future to use 'RegNotifyChangeKeyValue' which is exposed by the WinAPI and allows a process to register for notification on a registry key change. +- :remove: removes a value from an existing registry key + +### Attribute Parameters + +- key_name: name attribute. The registry key to create/modify. +- values: hash of the values to set under the registry key. The individual hash items will become respective 'Value name' => 'Value data' items in the registry key. +- type: Type of key to create, defaults to REG_SZ. Must be a symbol, see the overview below for valid values. + +### Registry key types + +- :binary: REG_BINARY +- :string: REG_SZ +- :multi_string: REG_MULTI_SZ +- :expand_string: REG_EXPAND_SZ +- :dword: REG_DWORD +- :dword_big_endian: REG_DWORD_BIG_ENDIAN +- :qword: REG_QWORD + +### Examples + + # make the local windows proxy match the one set for Chef + proxy = URI.parse(Chef::Config[:http_proxy]) + windows_registry 'HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings' do + values 'ProxyEnable' => 1, 'ProxyServer' => "#{proxy.host}:#{proxy.port}", 'ProxyOverride' => '' + end + + # enable Remote Desktop and poke the firewall hole + windows_registry 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' do + values 'FdenyTSConnections' => 0 + end + + # Delete an item from the registry + windows_registry 'HKCU\Software\Test' do + #Key is the name of the value that you want to delete the value is always empty + values 'ValueToDelete' => '' + action :remove + end + + # Add a REG_MULTI_SZ value to the registry + windows_registry 'HKCU\Software\Test' do + values 'MultiString' => ['line 1', 'line 2', 'line 3'] + type :multi_string + end + +### Library Methods + + Registry.value_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run','BGINFO') + Registry.key_exists?('HKLM\SOFTWARE\Microsoft') + BgInfo = Registry.get_value('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run','BGINFO') + +windows\_path +------------- + +### Actions + +- :add: Add an item to the system path +- :remove: Remove an item from the system path + +### Attribute Parameters + +- :path: Name attribute. The name of the value to add to the system path + +### Examples + + #Add Sysinternals to the system path + windows_path 'C:\Sysinternals' do + action :add + end + + #Remove 7-Zip from the system path + windows_path 'C:\7-Zip' do + action :remove + end + +windows\_task +------------- + +Creates, deletes or runs a Windows scheduled task. Requires Windows +Server 2008 due to API usage. + +### Actions + +- :create: creates a task +- :delete: deletes a task +- :run: runs a task +- :change: changes the un/pw or command of a task + +### Attribute Parameters + +- name: name attribute, The task name. +- command: The command the task will run. +- cwd: The directory the task will be run from. +- user: The user to run the task as. (requires password) +- password: The user's password. (requires user) +- run_level: Run with limited or highest privileges. +- frequency: Frequency with which to run the task. (hourly, daily, ect.) +- frequency_modifier: Multiple for frequency. (15 minutes, 2 days) + +### Examples + + # Run Chef every 15 minutes + windows_task "Chef client" do + user "Administrator" + password "$ecR3t" + cwd "C:\chef\bin" + command "chef-client -L C:\tmp\" + run_level :highest + frequency :minute + frequency_modifier 15 + end + + # Update Chef Client task with new password and log location + windows_task "Chef client" do + user "Administrator" + password "N3wPassW0Rd" + cwd "C:\chef\bin" + command "chef-client -L C:\chef\logs\" + action :change + end + + # Delete a taks named "old task" + windows_task "old task" do + action :delete + end + +windows\_zipfile +---------------- + +Most version of Windows do not ship with native cli utility for managing compressed files. This resource provides a pure-ruby implementation for managing zip files. Be sure to use the `not_if` or `only_if` meta parameters to guard the resource for idempotence or action will be taken on the zip file every Chef run. + +### Actions + +- :unzip: unzip a compressed file + +### Attribute Parameters + +- path: name attribute. The path where files will be unzipped to. +- source: The source of the zip file. This can either be a URI or a local path. +- overwrite: force an overwrite of the files if the already exists. +- checksum: useful if source is remote, the SHA-256 checksum of the file--if the local file matches the checksum, Chef will not download it + +### Examples + + # unzip a remote zip file locally + windows_zipfile "c:/bin" do + source "http://download.sysinternals.com/Files/SysinternalsSuite.zip" + action :unzip + not_if {::File.exists?("c:/bin/PsExec.exe")} + end + + # unzip a local zipfile + windows_zipfile "c:/the_codez" do + source "c:/foo/baz/the_codez.zip" + action :unzip + end + + +Exception/Report Handlers +========================= + +WindowsRebootHandler +-------------------- + +Required reboots are a necessary evil of configuring and managing Windows nodes. This report handler (ie fires at the end of successful Chef runs) acts on requested (Chef initiated) or pending (as determined by the OS per configuration action we performed) reboots. The `allow_pending_reboots` initialization argument should be set to false if you do not want the handler to automatically reboot a node if it has been determined a reboot is pending. Reboots can still be requested explicitly via the `windows_reboot` LWRP. + +## Initialization Arguments + +- `allow_pending_reboots`: indicator on whether the handler should act on a the Window's 'pending reboot' state. default is true +- `timeout`: timeout delay in seconds to wait before proceeding with the reboot. default is 60 seconds +- `reason`: comment on the reason for the reboot. default is 'Opscode Chef initiated reboot' + +Usage +===== + +Place an explicit dependency on this cookbook (using depends in the cookbook's metadata.rb) from any cookbook where you would like to use the Windows-specific resources/providers that ship with this cookbook. + + depends "windows" + +default +------- + +Convenience recipe that installs supporting gems for many of the resources/providers that ship with this cookbook. + +*Change in v1.3.0: Uses chef_gem instead of gem_package to ensure gem installation in Chef 0.10.10.* + +reboot\_handler +-------------- + +Leverages the `chef_handler` LWRP to register the `WindowsRebootHandler` report handler that ships as part of this cookbook. By default this handler is set to automatically act on pending reboots. If you would like to change this behavior override `node['windows']['allow_pending_reboots']` and set the value to false. For example: + + % cat roles/base.rb + name "base" + description "base role" + override_attributes( + "windows" => { + "allow_pending_reboots" => false + } + ) + +This will still allow a reboot to be explicitly requested via the `windows_reboot` LWRP. + +License and Author +================== + +Author:: Seth Chisamore () +Author:: Doug MacEachern () +Author:: Paul Morton () +Author:: Doug Ireton () + +Copyright:: 2011, Opscode, Inc. +Copyright:: 2010, VMware, Inc. +Copyright:: 2011, Business Intelligence Associates, Inc +Copyright:: 2012, Nordstrom, Inc. + + +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/windows/attributes/default.rb b/windows/attributes/default.rb new file mode 100644 index 0000000..b6788b1 --- /dev/null +++ b/windows/attributes/default.rb @@ -0,0 +1,22 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Attribute:: default +# +# Copyright 2011, Opscode, Inc +# +# 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. +# + +default['windows']['allow_pending_reboots'] = true +default['windows']['rubyzipversion'] = nil \ No newline at end of file diff --git a/windows/files/default/handlers/windows_reboot_handler.rb b/windows/files/default/handlers/windows_reboot_handler.rb new file mode 100644 index 0000000..e94dbb4 --- /dev/null +++ b/windows/files/default/handlers/windows_reboot_handler.rb @@ -0,0 +1,76 @@ +# +# Author:: Seth Chisamore () +# Copyright:: Copyright (c) 2011 Opscode, Inc +# License:: Apache License, Version 2.0 +# +# 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. +# + +class WindowsRebootHandler < Chef::Handler + include Chef::Mixin::ShellOut + + def initialize(allow_pending_reboots = true, timeout = 60, reason = "Opscode Chef initiated reboot") + @allow_pending_reboots = allow_pending_reboots + @timeout = timeout + @reason = reason + end + + def report + log_message, reboot = begin + if reboot_requested? + ["chef_handler[#{self.class}] requested reboot will occur in #{timeout} seconds", true] + elsif reboot_pending? + if @allow_pending_reboots + ["chef_handler[#{self.class}] reboot pending - automatic reboot will occur in #{timeout} seconds", true] + else + ["chef_handler[#{self.class}] reboot pending but handler not configured to act on pending reboots - please reboot node manually", false] + end + else + ["chef_handler[#{self.class}] no reboot requested or pending", false] + end + end + + Chef::Log.warn(log_message) + shell_out!("shutdown /r /t #{timeout} /c \"#{reason}\"") if reboot + end + + private + # reboot cause CHEF says so: + # reboot explicitly requested in our cookbook code + def reboot_requested? + node.run_state[:reboot_requested] == true + end + + # reboot cause WIN says so: + # reboot pending because of some configuration action we performed + def reboot_pending? + # Any files listed here means reboot needed + (Registry.key_exists?('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations') && + Registry.get_value('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager','PendingFileRenameOperations').any?) || + # 1 for any value means reboot pending + # "9306cdfc-c4a1-4a22-9996-848cb67eddc3"=1 + (Registry.key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') && + Registry.get_values('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired').select{|v| v[2] == 1 }.any?) || + # 1 or 2 for 'Flags' value means reboot pending + (Registry.key_exists?('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile') && + [1,2].include?(Registry::get_value('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile','Flags'))) + end + + def timeout + node.run_state[:reboot_timeout] || @timeout + end + + def reason + node.run_state[:reboot_reason] || @reason + end +end \ No newline at end of file diff --git a/windows/libraries/feature_base.rb b/windows/libraries/feature_base.rb new file mode 100644 index 0000000..66cbc42 --- /dev/null +++ b/windows/libraries/feature_base.rb @@ -0,0 +1,41 @@ +class Chef + class Provider + class WindowsFeature + module Base + + def action_install + unless installed? + install_feature(@new_resource.feature_name) + @new_resource.updated_by_last_action(true) + Chef::Log.info("#{@new_resource} installed feature") + else + Chef::Log.debug("#{@new_resource} is already installed - nothing to do") + end + end + + def action_remove + if installed? + remove_feature(@new_resource.feature_name) + @new_resource.updated_by_last_action(true) + Chef::Log.info("#{@new_resource} removed") + else + Chef::Log.debug("#{@new_resource} feature does not exist - nothing to do") + end + end + + def install_feature(name) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :install" + end + + def remove_feature(name) + raise Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :remove" + end + + def installed? + raise Chef::Exceptions::Override, "You must override installed? in #{self.to_s}" + end + end + end + end +end + \ No newline at end of file diff --git a/windows/libraries/helper.rb b/windows/libraries/helper.rb new file mode 100644 index 0000000..821d3df --- /dev/null +++ b/windows/libraries/helper.rb @@ -0,0 +1,88 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Library:: helper +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# +require 'uri' + +module Windows + module Helper + + AUTO_RUN_KEY = 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'.freeze unless defined?(AUTO_RUN_KEY) + ENV_KEY = 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'.freeze unless defined?(ENV_KEY) + + # returns windows friendly version of the provided path, + # ensures backslashes are used everywhere + def win_friendly_path(path) + path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) if path + end + + # account for Window's wacky File System Redirector + # http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx + # especially important for 32-bit processes (like Ruby) on a + # 64-bit instance of Windows. + def locate_sysnative_cmd(cmd) + if ::File.exists?("#{ENV['WINDIR']}\\sysnative\\#{cmd}") + "#{ENV['WINDIR']}\\sysnative\\#{cmd}" + elsif ::File.exists?("#{ENV['WINDIR']}\\system32\\#{cmd}") + "#{ENV['WINDIR']}\\system32\\#{cmd}" + else + cmd + end + end + + # Create a feature provider dependent value object. + # mainly created becasue Windows Feature names are + # different based on whether dism.exe or servicemanagercmd.exe + # is used for installation + def value_for_feature_provider(provider_hash) + p = Chef::Platform.find_provider_for_node(node, :windows_feature) + key = p.to_s.downcase.split('::').last + provider_hash[key] || provider_hash[key.to_sym] + end + + # singleton instance of the Windows Version checker + def win_version + @win_version ||= Windows::Version.new + end + + # if a file is local it returns a windows friendly path version + # if a file is remote it caches it locally + def cached_file(source, checksum=nil, windows_path=true) + @installer_file_path ||= begin + + if source =~ ::URI::ABS_URI && %w[http https].include?(URI.parse(source).scheme) + uri = ::URI.parse(::URI.unescape(source)) + cache_file_path = "#{Chef::Config[:file_cache_path]}/#{::File.basename(uri.path)}" + Chef::Log.debug("Caching a copy of file #{source} at #{cache_file_path}") + r = Chef::Resource::RemoteFile.new(cache_file_path, run_context) + r.source(source) + r.backup(false) + r.checksum(checksum) if checksum + r.run_action(:create) + else + cache_file_path = source + end + + windows_path ? win_friendly_path(cache_file_path) : cache_file_path + end + end + + end +end + +Chef::Recipe.send(:include, Windows::Helper) diff --git a/windows/libraries/registry_helper.rb b/windows/libraries/registry_helper.rb new file mode 100644 index 0000000..1b3a69c --- /dev/null +++ b/windows/libraries/registry_helper.rb @@ -0,0 +1,357 @@ +# +# Author:: Doug MacEachern () +# Author:: Seth Chisamore () +# Author:: Paul Morton () +# Cookbook Name:: windows +# Provider:: registry +# +# Copyright:: 2010, VMware, Inc. +# Copyright:: 2011, Opscode, Inc. +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'win32/registry' + require 'ruby-wmi' +end + +module Windows + module RegistryHelper + + @@native_registry_constant = ENV['PROCESSOR_ARCHITEW6432'] == 'AMD64' ? 0x0100 : 0x0200 + + def get_hive_name(path) + Chef::Log.debug("Resolving registry shortcuts to full names") + + reg_path = path.split("\\") + hive_name = reg_path.shift + + hkey = { + "HKLM" => "HKEY_LOCAL_MACHINE", + "HKCU" => "HKEY_CURRENT_USER", + "HKU" => "HKEY_USERS" + }[hive_name] || hive_name + + Chef::Log.debug("Hive resolved to #{hkey}") + return hkey + end + + def get_hive(path) + + Chef::Log.debug("Getting hive for #{path}") + reg_path = path.split("\\") + hive_name = reg_path.shift + + hkey = get_hive_name(path) + + hive = { + "HKEY_LOCAL_MACHINE" => ::Win32::Registry::HKEY_LOCAL_MACHINE, + "HKEY_USERS" => ::Win32::Registry::HKEY_USERS, + "HKEY_CURRENT_USER" => ::Win32::Registry::HKEY_CURRENT_USER + }[hkey] + + unless hive + Chef::Application.fatal!("Unsupported registry hive '#{hive_name}'") + end + + + Chef::Log.debug("Registry hive resolved to #{hkey}") + return hive + end + + def unload_hive(path) + hive = get_hive(path) + if hive == ::Win32::Registry::HKEY_USERS + reg_path = path.split("\\") + priv = Chef::WindowsPrivileged.new + begin + priv.reg_unload_key(reg_path[1]) + rescue + end + end + end + + def set_value(mode,path,values,type=nil) + hive, reg_path, hive_name, root_key, hive_loaded = get_reg_path_info(path) + key_name = reg_path.join("\\") + + Chef::Log.debug("Creating #{path}") + + if !key_exists?(path,true) + create_key(path) + end + + hive.send(mode, key_name, ::Win32::Registry::KEY_ALL_ACCESS | @@native_registry_constant) do |reg| + changed_something = false + values.each do |k,val| + key = "#{k}" #wtf. avoid "can't modify frozen string" in win32/registry.rb + cur_val = nil + begin + cur_val = reg[key] + rescue + #subkey does not exist (ok) + end + if cur_val != val + Chef::Log.debug("setting #{key}=#{val}") + + if type.nil? + type = :string + end + + reg_type = { + :binary => ::Win32::Registry::REG_BINARY, + :string => ::Win32::Registry::REG_SZ, + :multi_string => ::Win32::Registry::REG_MULTI_SZ, + :expand_string => ::Win32::Registry::REG_EXPAND_SZ, + :dword => ::Win32::Registry::REG_DWORD, + :dword_big_endian => ::Win32::Registry::REG_DWORD_BIG_ENDIAN, + :qword => ::Win32::Registry::REG_QWORD + }[type] + + reg.write(key, reg_type, val) + + ensure_hive_unloaded(hive_loaded) + + changed_something = true + end + end + return changed_something + end + return false + end + + def get_value(path,value) + hive, reg_path, hive_name, root_key, hive_loaded = get_reg_path_info(path) + key = reg_path.join("\\") + + hive.open(key, ::Win32::Registry::KEY_ALL_ACCESS | @@native_registry_constant) do | reg | + begin + return reg[value] + rescue + return nil + ensure + ensure_hive_unloaded(hive_loaded) + end + end + end + + def get_values(path) + hive, reg_path, hive_name, root_key, hive_loaded = get_reg_path_info(path) + key = reg_path.join("\\") + hive.open(key, ::Win32::Registry::KEY_ALL_ACCESS | @@native_registry_constant) do | reg | + values = [] + begin + reg.each_value do |name, type, data| + values << [name, type, data] + end + rescue + ensure + ensure_hive_unloaded(hive_loaded) + end + values + end + end + + def delete_value(path,values) + hive, reg_path, hive_name, root_key, hive_loaded = get_reg_path_info(path) + key = reg_path.join("\\") + Chef::Log.debug("Deleting values in #{path}") + hive.open(key, ::Win32::Registry::KEY_ALL_ACCESS | @@native_registry_constant) do | reg | + values.each_key { |key| + name = "#{key}" + # Ensure delete operation is idempotent. + if value_exists?(path, key) + Chef::Log.debug("Deleting value #{name} in #{path}") + reg.delete_value(name) + else + Chef::Log.debug("Value #{name} in #{path} does not exist, skipping.") + end + } + end + + end + + def create_key(path) + hive, reg_path, hive_name, root_key, hive_loaded = get_reg_path_info(path) + key = reg_path.join("\\") + Chef::Log.debug("Creating registry key #{path}") + hive.create(key) + end + + def value_exists?(path,value) + if key_exists?(path,true) + + hive, reg_path, hive_name, root_key , hive_loaded = get_reg_path_info(path) + key = reg_path.join("\\") + + Chef::Log.debug("Attempting to open #{key}"); + Chef::Log.debug("Native Constant #{@@native_registry_constant}") + Chef::Log.debug("Hive #{hive}") + + hive.open(key, ::Win32::Registry::KEY_READ | @@native_registry_constant) do | reg | + begin + rtn_value = reg[value] + return true + rescue + return false + ensure + ensure_hive_unloaded(hive_loaded) + end + end + + end + return false + end + + # TODO: Does not load user registry... + def key_exists?(path, load_hive = false) + if load_hive + hive, reg_path, hive_name, root_key , hive_loaded = get_reg_path_info(path) + key = reg_path.join("\\") + else + hive = get_hive(path) + reg_path = path.split("\\") + hive_name = reg_path.shift + root_key = reg_path[0] + key = reg_path.join("\\") + hive_loaded = false + end + + begin + hive.open(key, ::Win32::Registry::Constants::KEY_READ | @@native_registry_constant ) + return true + rescue + return false + ensure + ensure_hive_unloaded(hive_loaded) + end + end + + def get_user_hive_location(sid) + reg_key = "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\#{sid}" + Chef::Log.debug("Looking for profile at #{reg_key}") + if key_exists?(reg_key) + return get_value(reg_key,'ProfileImagePath') + else + return nil + end + + end + + def resolve_user_to_sid(username) + begin + sid = WMI::Win32_UserAccount.find(:first, :conditions => {:name => username}).sid + Chef::Log.debug("Resolved user SID to #{sid}") + return sid + rescue + return nil + end + end + + def hive_loaded?(path) + hive = get_hive(path) + reg_path = path.split("\\") + hive_name = reg_path.shift + user_hive = path[0] + + if is_user_hive?(hive) + return key_exists?("#{hive_name}\\#{user_hive}") + else + return true + end + end + + def is_user_hive?(hive) + if hive == ::Win32::Registry::HKEY_USERS + return true + else + return true + end + end + + def get_reg_path_info(path) + hive = get_hive(path) + reg_path = path.split("\\") + hive_name = reg_path.shift + root_key = reg_path[0] + hive_loaded = false + + if is_user_hive?(hive) && !key_exists?("#{hive_name}\\#{root_key}") + reg_path, hive_loaded = load_user_hive(hive,reg_path,root_key) + root_key = reg_path[0] + Chef::Log.debug("Resolved user (#{path}) to (#{reg_path.join('/')})") + end + + return hive, reg_path, hive_name, root_key, hive_loaded + end + + def load_user_hive(hive,reg_path,user_hive) + Chef::Log.debug("Reg Path #{reg_path}") + # See if the hive is loaded. Logged in users will have a key that is named their SID + # if the user has specified the a path by SID and the user is logged in, this function + # should not be executed. + if is_user_hive?(hive) && !key_exists?("HKU\\#{user_hive}") + Chef::Log.debug("The user is not logged in and has not been specified by SID") + sid = resolve_user_to_sid(user_hive) + Chef::Log.debug("User SID resolved to (#{sid})") + # Now that the user has been resolved to a SID, check and see if the hive exists. + # If this exists by SID, the user is logged in and we should use that key. + # TODO: Replace the username with the sid and send it back because the username + # does not exist as the key location. + load_reg = false + if key_exists?("HKU\\#{sid}") + reg_path[0] = sid #use the active profile (user is logged on) + Chef::Log.debug("HKEY_USERS Mapped: #{user_hive} -> #{sid}") + else + Chef::Log.debug("User is not logged in") + load_reg = true + end + + # The user is not logged in, so we should load the registry from disk + if load_reg + profile_path = get_user_hive_location(sid) + if profile_path != nil + ntuser_dat = "#{profile_path}\\NTUSER.DAT" + if ::File.exists?(ntuser_dat) + priv = Chef::WindowsPrivileged.new + if priv.reg_load_key(sid,ntuser_dat) + Chef::Log.debug("RegLoadKey(#{sid}, #{user_hive}, #{ntuser_dat})") + reg_path[0] = sid + else + Chef::Log.debug("Failed RegLoadKey(#{sid}, #{user_hive}, #{ntuser_dat})") + end + end + end + end + end + + return reg_path, load_reg + + end + + private + def ensure_hive_unloaded(hive_loaded=false) + if(hive_loaded) + Chef::Log.debug("Hive was loaded, we really should unload it") + unload_hive(path) + end + end + end +end + +module Registry + module_function + extend Windows::RegistryHelper +end diff --git a/windows/libraries/version.rb b/windows/libraries/version.rb new file mode 100644 index 0000000..c3c17c0 --- /dev/null +++ b/windows/libraries/version.rb @@ -0,0 +1,204 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Library:: version +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'ruby-wmi' + require 'Win32API' +end + +module Windows + class Version + + # http://msdn.microsoft.com/en-us/library/ms724833(v=vs.85).aspx + + # Suite Masks + # Microsoft BackOffice components are installed. + VER_SUITE_BACKOFFICE = 0x00000004.freeze unless defined?(VER_SUITE_BACKOFFICE) + # Windows Server 2003, Web Edition is installed. + VER_SUITE_BLADE = 0x00000400.freeze unless defined?(VER_SUITE_BLADE) + # Windows Server 2003, Compute Cluster Edition is installed. + VER_SUITE_COMPUTE_SERVER = 0x00004000.freeze unless defined?(VER_SUITE_COMPUTE_SERVER) + # Windows Server 2008 Datacenter, Windows Server 2003, Datacenter Edition, or Windows 2000 Datacenter Server is installed. + VER_SUITE_DATACENTER = 0x00000080.freeze unless defined?(VER_SUITE_DATACENTER) + # Windows Server 2008 Enterprise, Windows Server 2003, Enterprise Edition, or Windows 2000 Advanced Server is installed. Refer to the Remarks section for more information about this bit flag. + VER_SUITE_ENTERPRISE = 0x00000002.freeze unless defined?(VER_SUITE_ENTERPRISE) + # Windows XP Embedded is installed. + VER_SUITE_EMBEDDEDNT = 0x00000040.freeze unless defined?(VER_SUITE_EMBEDDEDNT) + # Windows Vista Home Premium, Windows Vista Home Basic, or Windows XP Home Edition is installed. + VER_SUITE_PERSONAL = 0x00000200.freeze unless defined?(VER_SUITE_PERSONAL) + # Remote Desktop is supported, but only one interactive session is supported. This value is set unless the system is running in application server mode. + VER_SUITE_SINGLEUSERTS = 0x00000100.freeze unless defined?(VER_SUITE_SINGLEUSERTS) + # Microsoft Small Business Server was once installed on the system, but may have been upgraded to another version of Windows. Refer to the Remarks section for more information about this bit flag. + VER_SUITE_SMALLBUSINESS = 0x00000001.freeze unless defined?(VER_SUITE_SMALLBUSINESS) + # Microsoft Small Business Server is installed with the restrictive client license in force. Refer to the Remarks section for more information about this bit flag. + VER_SUITE_SMALLBUSINESS_RESTRICTED = 0x00000020.freeze unless defined?(VER_SUITE_SMALLBUSINESS_RESTRICTED) + # Windows Storage Server 2003 R2 or Windows Storage Server 2003is installed. + VER_SUITE_STORAGE_SERVER = 0x00002000.freeze unless defined?(VER_SUITE_STORAGE_SERVER) + # Terminal Services is installed. This value is always set. + # If VER_SUITE_TERMINAL is set but VER_SUITE_SINGLEUSERTS is not set, the system is running in application server mode. + VER_SUITE_TERMINAL = 0x00000010.freeze unless defined?(VER_SUITE_TERMINAL) + # Windows Home Server is installed. + VER_SUITE_WH_SERVER = 0x00008000.freeze unless defined?(VER_SUITE_WH_SERVER) + + # Product Type + # The system is a domain controller and the operating system is Windows Server 2008 R2, Windows Server 2008, Windows Server 2003, or Windows 2000 Server. + VER_NT_DOMAIN_CONTROLLER = 0x0000002.freeze unless defined?(VER_NT_DOMAIN_CONTROLLER) + # The operating system is Windows Server 2008 R2, Windows Server 2008, Windows Server 2003, or Windows 2000 Server. + # Note that a server that is also a domain controller is reported as VER_NT_DOMAIN_CONTROLLER, not VER_NT_SERVER. + VER_NT_SERVER = 0x0000003.freeze unless defined?(VER_NT_SERVER) + # The operating system is Windows 7, Windows Vista, Windows XP Professional, Windows XP Home Edition, or Windows 2000 Professional. + VER_NT_WORKSTATION = 0x0000001.freeze unless defined?(VER_NT_WORKSTATION) + + # GetSystemMetrics + # The build number if the system is Windows Server 2003 R2; otherwise, 0. + SM_SERVERR2 = 89.freeze unless defined?(SM_SERVERR2) + + # http://msdn.microsoft.com/en-us/library/ms724358(v=vs.85).aspx + # this is what it sounds like...when kittens die + SKU = { + 0x00000006 => {:ms_const => 'PRODUCT_BUSINESS', :name => 'Business'}, + 0x00000010 => {:ms_const => 'PRODUCT_BUSINESS_N', :name => 'Business N'}, + 0x00000012 => {:ms_const => 'PRODUCT_CLUSTER_SERVER', :name => 'HPC Edition'}, + 0x00000008 => {:ms_const => 'PRODUCT_DATACENTER_SERVER', :name => 'Server Datacenter (full installation)'}, + 0x0000000C => {:ms_const => 'PRODUCT_DATACENTER_SERVER_CORE', :name => 'Server Datacenter (core installation)'}, + 0x00000027 => {:ms_const => 'PRODUCT_DATACENTER_SERVER_CORE_V', :name => 'Server Datacenter without Hyper-V (core installation)'}, + 0x00000025 => {:ms_const => 'PRODUCT_DATACENTER_SERVER_V', :name => 'Server Datacenter without Hyper-V (full installation)'}, + 0x00000004 => {:ms_const => 'PRODUCT_ENTERPRISE', :name => 'Enterprise'}, + 0x00000046 => {:ms_const => 'PRODUCT_ENTERPRISE_E', :name => 'Not supported'}, + 0x0000001B => {:ms_const => 'PRODUCT_ENTERPRISE_N', :name => 'Enterprise N'}, + 0x0000000A => {:ms_const => 'PRODUCT_ENTERPRISE_SERVER', :name => 'Server Enterprise (full installation)'}, + 0x0000000E => {:ms_const => 'PRODUCT_ENTERPRISE_SERVER_CORE', :name => 'Server Enterprise (core installation)'}, + 0x00000029 => {:ms_const => 'PRODUCT_ENTERPRISE_SERVER_CORE_V', :name => 'Server Enterprise without Hyper-V (core installation)'}, + 0x0000000F => {:ms_const => 'PRODUCT_ENTERPRISE_SERVER_IA64', :name => 'Server Enterprise for Itanium-based Systems'}, + 0x00000026 => {:ms_const => 'PRODUCT_ENTERPRISE_SERVER_V', :name => 'Server Enterprise without Hyper-V (full installation)'}, + 0x00000002 => {:ms_const => 'PRODUCT_HOME_BASIC', :name => 'Home Basic'}, + 0x00000043 => {:ms_const => 'PRODUCT_HOME_BASIC_E', :name => 'Not supported'}, + 0x00000005 => {:ms_const => 'PRODUCT_HOME_BASIC_N', :name => 'Home Basic N'}, + 0x00000003 => {:ms_const => 'PRODUCT_HOME_PREMIUM', :name => 'Home Premium'}, + 0x00000044 => {:ms_const => 'PRODUCT_HOME_PREMIUM_E', :name => 'Not supported'}, + 0x0000001A => {:ms_const => 'PRODUCT_HOME_PREMIUM_N', :name => 'Home Premium N'}, + 0x0000002A => {:ms_const => 'PRODUCT_HYPERV', :name => 'Microsoft Hyper-V Server'}, + 0x0000001E => {:ms_const => 'PRODUCT_MEDIUMBUSINESS_SERVER_MANAGEMENT', :name => 'Windows Essential Business Server Management Server'}, + 0x00000020 => {:ms_const => 'PRODUCT_MEDIUMBUSINESS_SERVER_MESSAGING', :name => 'Windows Essential Business Server Messaging Server'}, + 0x0000001F => {:ms_const => 'PRODUCT_MEDIUMBUSINESS_SERVER_SECURITY', :name => 'Windows Essential Business Server Security Server'}, + 0x00000030 => {:ms_const => 'PRODUCT_PROFESSIONAL', :name => 'Professional'}, + 0x00000045 => {:ms_const => 'PRODUCT_PROFESSIONAL_E', :name => 'Not supported'}, + 0x00000031 => {:ms_const => 'PRODUCT_PROFESSIONAL_N', :name => 'Professional N'}, + 0x00000018 => {:ms_const => 'PRODUCT_SERVER_FOR_SMALLBUSINESS', :name => 'Windows Server 2008 for Windows Essential Server Solutions'}, + 0x00000023 => {:ms_const => 'PRODUCT_SERVER_FOR_SMALLBUSINESS_V', :name => 'Windows Server 2008 without Hyper-V for Windows Essential Server Solutions'}, + 0x00000021 => {:ms_const => 'PRODUCT_SERVER_FOUNDATION', :name => 'Server Foundation'}, + 0x00000022 => {:ms_const => 'PRODUCT_HOME_PREMIUM_SERVER', :name => 'Windows Home Server 2011'}, + 0x00000032 => {:ms_const => 'PRODUCT_SB_SOLUTION_SERVER', :name => 'Windows Small Business Server 2011 Essentials'}, + 0x00000013 => {:ms_const => 'PRODUCT_HOME_SERVER', :name => 'Windows Storage Server 2008 R2 Essentials'}, + 0x00000009 => {:ms_const => 'PRODUCT_SMALLBUSINESS_SERVER', :name => 'Windows Small Business Server'}, + 0x00000038 => {:ms_const => 'PRODUCT_SOLUTION_EMBEDDEDSERVER', :name => 'Windows MultiPoint Server'}, + 0x00000007 => {:ms_const => 'PRODUCT_STANDARD_SERVER', :name => 'Server Standard (full installation)'}, + 0x0000000D => {:ms_const => 'PRODUCT_STANDARD_SERVER_CORE', :name => 'Server Standard (core installation)'}, + 0x00000028 => {:ms_const => 'PRODUCT_STANDARD_SERVER_CORE_V', :name => 'Server Standard without Hyper-V (core installation)'}, + 0x00000024 => {:ms_const => 'PRODUCT_STANDARD_SERVER_V', :name => 'Server Standard without Hyper-V (full installation)'}, + 0x0000000B => {:ms_const => 'PRODUCT_STARTER', :name => 'Starter'}, + 0x00000042 => {:ms_const => 'PRODUCT_STARTER_E', :name => 'Not supported'}, + 0x0000002F => {:ms_const => 'PRODUCT_STARTER_N', :name => 'Starter N'}, + 0x00000017 => {:ms_const => 'PRODUCT_STORAGE_ENTERPRISE_SERVER', :name => 'Storage Server Enterprise'}, + 0x00000014 => {:ms_const => 'PRODUCT_STORAGE_EXPRESS_SERVER', :name => 'Storage Server Express'}, + 0x00000015 => {:ms_const => 'PRODUCT_STORAGE_STANDARD_SERVER', :name => 'Storage Server Standard'}, + 0x00000016 => {:ms_const => 'PRODUCT_STORAGE_WORKGROUP_SERVER', :name => 'Storage Server Workgroup'}, + 0x00000000 => {:ms_const => 'PRODUCT_UNDEFINED', :name => 'An unknown product'}, + 0x00000001 => {:ms_const => 'PRODUCT_ULTIMATE', :name => 'Ultimate'}, + 0x00000047 => {:ms_const => 'PRODUCT_ULTIMATE_E', :name => 'Not supported'}, + 0x0000001C => {:ms_const => 'PRODUCT_ULTIMATE_N', :name => 'Ultimate N'}, + 0x00000011 => {:ms_const => 'PRODUCT_WEB_SERVER', :name => 'Web Server (full installation)'}, + 0x0000001D => {:ms_const => 'PRODUCT_WEB_SERVER_CORE', :name => 'Web Server (core installation)'} + }.freeze unless defined?(SKU) + + attr_reader :major_version, :minor_version, :build_number, :service_pack_major_version, :service_pack_minor_version + attr_reader :version, :product_type, :product_suite, :sku + + def initialize + unless RUBY_PLATFORM =~ /mswin|mingw32|windows/ + raise NotImplementedError, 'only valid on Windows platform' + end + @version, @product_type, @product_suite, @sku, @service_pack_major_version, @service_pack_minor_version = get_os_info + @major_version, @minor_version, @build_number = version.split('.').map{|v| v.to_i } + end + + WIN_VERSIONS = { + "Windows 7" => {:major => 6, :minor => 1, :callable => lambda{ @product_type == VER_NT_WORKSTATION }}, + "Windows Server 2008 R2" => {:major => 6, :minor => 1, :callable => lambda{ @product_type != VER_NT_WORKSTATION }}, + "Windows Server 2008" => {:major => 6, :minor => 0, :callable => lambda{ @product_type != VER_NT_WORKSTATION }}, + "Windows Vista" => {:major => 6, :minor => 0, :callable => lambda{ @product_type == VER_NT_WORKSTATION }}, + "Windows Server 2003 R2" => {:major => 5, :minor => 2, :callable => lambda{ Win32API.new('user32', 'GetSystemMetrics', 'I', 'I').call(SM_SERVERR2) != 0 }}, + "Windows Home Server" => {:major => 5, :minor => 2, :callable => lambda{ (@product_suite & VER_SUITE_WH_SERVER) == VER_SUITE_WH_SERVER }}, + "Windows Server 2003" => {:major => 5, :minor => 2, :callable => lambda{ Win32API.new('user32', 'GetSystemMetrics', 'I', 'I').call(SM_SERVERR2) == 0 }}, + "Windows XP" => {:major => 5, :minor => 1}, + "Windows 2000" => {:major => 5, :minor => 0} + }.freeze unless defined?(WIN_VERSIONS) + + marketing_names = Array.new + + # General Windows checks + WIN_VERSIONS.each do |k,v| + method_name = "#{k.gsub(/\s/, '_').downcase}?" + define_method(method_name) do + (@major_version == v[:major]) && + (@minor_version == v[:minor]) && + (v[:callable] ? v[:callable].call : true) + end + marketing_names << [k, method_name] + end + + define_method(:marketing_name) do + marketing_names.each do |mn| + break mn[0] if self.send(mn[1]) + end + end + + # Server Type checks + %w{ core full datacenter }.each do |m| + define_method("server_#{m}?") do + if @sku + !(SKU[@sku][:name] =~ /#{m}/i).nil? + else + false + end + end + end + + private + # Win32API call to GetSystemMetrics(SM_SERVERR2) + # returns: The build number if the system is Windows Server 2003 R2; otherwise, 0. + def sm_serverr2 + @sm_serverr2 ||= Win32API.new('user32', 'GetSystemMetrics', 'I', 'I').call(SM_SERVERR2) + end + + # query WMI Win32_OperatingSystem for required OS info + def get_os_info + cols = %w{ Version ProductType OSProductSuite OperatingSystemSKU ServicePackMajorVersion ServicePackMinorVersion } + os_info = WMI::Win32_OperatingSystem.find(:first) + cols.map do |c| + begin + os_info.send(c) + rescue # OperatingSystemSKU doesn't exist in all versions of Windows + nil + end + end + end + end +end diff --git a/windows/libraries/windows_privileged.rb b/windows/libraries/windows_privileged.rb new file mode 100644 index 0000000..2b3753d --- /dev/null +++ b/windows/libraries/windows_privileged.rb @@ -0,0 +1,94 @@ +# +# Author:: Doug MacEachern +# Author:: Paul Morton () +# Cookbook Name:: windows +# Library:: windows_privileged +# +# Copyright:: 2010, VMware, Inc. +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'windows/error' + require 'windows/registry' + require 'windows/process' + require 'windows/security' +end + +#helpers for Windows API calls that require privilege adjustments +class Chef + class WindowsPrivileged + if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Windows::Error + include Windows::Registry + include Windows::Process + include Windows::Security + end + #File -> Load Hive... in regedit.exe + def reg_load_key(name, file) + run(SE_BACKUP_NAME, SE_RESTORE_NAME) do + rc = RegLoadKey(HKEY_USERS, "#{name}", file) + if rc == ERROR_SUCCESS + return true + elsif rc == ERROR_SHARING_VIOLATION + return false + else + raise get_last_error(rc) + end + end + end + + #File -> Unload Hive... in regedit.exe + def reg_unload_key(name) + run(SE_BACKUP_NAME, SE_RESTORE_NAME) do + rc = RegUnLoadKey(HKEY_USERS, "#{name}") + if rc != ERROR_SUCCESS + raise get_last_error(rc) + end + end + end + + def run(*privileges) + token = [0].pack('L') + + unless OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, token) + raise get_last_error + end + token = token.unpack('L')[0] + + privileges.each do |name| + unless adjust_privilege(token, name, SE_PRIVILEGE_ENABLED) + raise get_last_error + end + end + + begin + yield + ensure #disable privs + privileges.each do |name| + adjust_privilege(token, name, 0) + end + end + end + + def adjust_privilege(token, priv, attr=0) + luid = [0,0].pack('Ll') + if LookupPrivilegeValue(nil, priv, luid) + new_state = [1, luid.unpack('Ll'), attr].flatten.pack('LLlL') + AdjustTokenPrivileges(token, 0, new_state, new_state.size, 0, 0) + end + end + end +end \ No newline at end of file diff --git a/windows/metadata.json b/windows/metadata.json new file mode 100644 index 0000000..c9daefa --- /dev/null +++ b/windows/metadata.json @@ -0,0 +1,31 @@ +{ + "name": "windows", + "description": "Provides a set of useful Windows-specific primitives.", + "long_description": "Description\n===========\n\nProvides a set of Windows-specific primitives (Chef resources) meant to aid in the creation of cookbooks/recipes targeting the Windows platform.\n\nRequirements\n============\n\nVersion 1.3.0+ of this cookbook requires Chef 0.10.10+.\n\nPlatform\n--------\n\n* Windows XP\n* Windows Vista\n* Windows Server 2003 R2\n* Windows 7\n* Windows Server 2008 (R1, R2)\n\nThe `windows_task` LWRP requires Windows Server 2008 due to its API usage.\n\nCookbooks\n---------\n\nThe following cookbooks provided by Opscode are required as noted:\n\n* chef_handler (`windows::reboot_handler` leverages the chef_handler LWRP)\n* powershell - The Printer and Printer Port LWRP require Powershell.\n\n**NOTE** We cannot specifically depend on Opscode's powershell,\n because powershell depends on this cookbook. Ensure that\n `recipe[powershell]` exists in the node's expanded run list so it\n gets downloaded where the printer LWRPs are used.\n\nAttributes\n==========\n\n* `node['windows']['allow_pending_reboots']` - used to configure the `WindowsRebootHandler` (via the `windows::reboot_handler` recipe) to act on pending reboots. default is true (ie act on pending reboots). The value of this attribute only has an effect if the `windows::reboot_handler` is in a node's run list.\n\nResource/Provider\n=================\n\nwindows\\_auto\\_run\n------------------\n\n### Actions\n\n- :create: Create an item to be run at login\n- :remove: Remove an item that was previously setup to run at login\n\n### Attribute Parameters\n\n- :name: Name attribute. The name of the value to be stored in the registry\n- :program: The program to be run at login\n- :args: The arguments for the program\n\n### Examples\n\n # Run BGInfo at login\n windows_auto_run 'BGINFO' do\n program \"C:/Sysinternals/bginfo.exe\"\n args \"\\\"C:/Sysinternals/Config.bgi\\\" /NOLICPROMPT /TIMER:0\"\n not_if { Registry.value_exists?(AUTO_RUN_KEY, 'BGINFO') }\n action :create\n end\n\n\nwindows\\_batch\n--------------\n\nExecute a batch script using the cmd.exe interpreter (much like the script resources for bash, csh, powershell, perl, python and ruby). A temporary file is created and executed like other script resources, rather than run inline. By their nature, Script resources are not idempotent, as they are completely up to the user's imagination. Use the `not_if` or `only_if` meta parameters to guard the resource for idempotence.\n\n### Actions\n\n- :run: run the batch file\n\n### Attribute Parameters\n\n- command: name attribute. Name of the command to execute.\n- code: quoted string of code to execute.\n- creates: a file this command creates - if the file exists, the command will not be run.\n- cwd: current working directory to run the command from.\n- flags: command line flags to pass to the interpreter when invoking.\n- user: A user name or user ID that we should change to before running this command.\n- group: A group name or group ID that we should change to before running this command.\n\n### Examples\n\n windows_batch \"unzip_and_move_ruby\" do\n code <<-EOH\n 7z.exe x #{Chef::Config[:file_cache_path]}/ruby-1.8.7-p352-i386-mingw32.7z -oC:\\\\source -r -y\n xcopy C:\\\\source\\\\ruby-1.8.7-p352-i386-mingw32 C:\\\\ruby /e /y\n EOH\n end\n\n windows_batch \"echo some env vars\" do\n code <<-EOH\n echo %TEMP%\n echo %SYSTEMDRIVE%\n echo %PATH%\n echo %WINDIR%\n EOH\n end\n\nwindows\\_feature\n----------------\n\nWindows Roles and Features can be thought of as built-in operating system packages that ship with the OS. A server role is a set of software programs that, when they are installed and properly configured, lets a computer perform a specific function for multiple users or other computers within a network. A Role can have multiple Role Services that provide functionality to the Role. Role services are software programs that provide the functionality of a role. Features are software programs that, although they are not directly parts of roles, can support or augment the functionality of one or more roles, or improve the functionality of the server, regardless of which roles are installed. Collectively we refer to all of these attributes as 'features'.\n\nThis resource allows you to manage these 'features' in an unattended, idempotent way.\n\nThere are two providers for the `windows_features` which map into Microsoft's two major tools for managing roles/features: [Deployment Image Servicing and Management (DISM)](http://msdn.microsoft.com/en-us/library/dd371719(v=vs.85).aspx) and [Servermanagercmd](http://technet.microsoft.com/en-us/library/ee344834(WS.10).aspx) (The CLI for Server Manager). As Servermanagercmd is deprecated, Chef will set the default provider to `Chef::Provider::WindowsFeature::DISM` if DISM is present on the system being configured. The default provider will fall back to `Chef::Provider::WindowsFeature::ServerManagerCmd`.\n\nFor more information on Roles, Role Services and Features see the [Microsoft TechNet article on the topic](http://technet.microsoft.com/en-us/library/cc754923.aspx). For a complete list of all features that are available on a node type either of the following commands at a command prompt:\n\n dism /online /Get-Features\n servermanagercmd -query\n\n### Actions\n\n- :install: install a Windows role/feature\n- :remove: remove a Windows role/feature\n\n### Attribute Parameters\n\n- feature_name: name of the feature/role to install. The same feature may have different names depending on the provider used (ie DHCPServer vs DHCP; DNS-Server-Full-Role vs DNS).\n\n### Providers\n\n- **Chef::Provider::WindowsFeature::DISM**: Uses Deployment Image Servicing and Management (DISM) to manage roles/features.\n- **Chef::Provider::WindowsFeature::ServerManagerCmd**: Uses Server Manager to manage roles/features.\n\n### Examples\n\n # enable the node as a DHCP Server\n windows_feature \"DHCPServer\" do\n action :install\n end\n\n # enable TFTP\n windows_feature \"TFTP\" do\n action :install\n end\n\n # disable Telnet client/server\n %w{ TelnetServer TelnetClient }.each do |feature|\n windows_feature feature do\n action :remove\n end\n end\n\nwindows\\_package\n----------------\n\nManage Windows application packages in an unattended, idempotent way.\n\nThe following application installers are currently supported:\n\n* MSI packages\n* InstallShield\n* Wise InstallMaster\n* Inno Setup\n* Nullsoft Scriptable Install System\n\nIf the proper installer type is not passed into the resource's installer_type attribute, the provider will do it's best to identify the type by introspecting the installation package. If the installation type cannot be properly identified the `:custom` value can be passed into the installer_type attribute along with the proper flags for silent/quiet installation (using the `options` attribute..see example below).\n\n__PLEASE NOTE__ - For proper idempotence the resource's `package_name` should be the same as the 'DisplayName' registry value in the uninstallation data that is created during package installation. The easiest way to definitively find the proper 'DisplayName' value is to install the package on a machine and search for the uninstall information under the following registry keys:\n\n* `HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall`\n* `HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall`\n* `HKEY_LOCAL_MACHINE\\Software\\Wow6464Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall`\n\nFor maximum flexibility the `source` attribute supports both remote and local installation packages.\n\n### Actions\n\n- :install: install a package\n- :remove: remove a package. The remove action is completely hit or miss as many application uninstallers do not support a full silent/quiet mode.\n\n### Attribute Parameters\n\n- package_name: name attribute. The 'DisplayName' of the application installation package.\n- source: The source of the windows installer. This can either be a URI or a local path.\n- installer_type: They type of windows installation package. valid values are: :msi, :inno, :nsis, :wise, :installshield, :custom. If this value is not provided, the provider will do it's best to identify the installer type through introspection of the file.\n- checksum: useful if source is remote, the SHA-256 checksum of the file--if the local file matches the checksum, Chef will not download it\n- options: Additional options to pass the underlying installation command\n- timeout: set a timeout for the package download (default 600 seconds)\n- version: The version number of this package, as indicated by the 'DisplayVersion' value in one of the 'Uninstall' registry keys. If the given version number does equal the 'DisplayVersion' in the registry, the package will be installed.\n- success_codes: set an array of possible successful installation\n return codes. Previously this was hardcoded, but certain MSIs may\n have a different return code, e.g. 3010 for reboot required. Must be\n an array, and defaults to `[0, 42, 127]`.\n\n### Examples\n\n # install PuTTY (InnoSetup installer)\n windows_package \"PuTTY version 0.60\" do\n source \"http://the.earth.li/~sgtatham/putty/latest/x86/putty-0.60-installer.exe\"\n installer_type :inno\n action :install\n end\n\n # install 7-Zip (MSI installer)\n windows_package \"7-Zip 9.20 (x64 edition)\" do\n source \"http://downloads.sourceforge.net/sevenzip/7z920-x64.msi\"\n action :install\n end\n\n # install Notepad++ (Y U No Emacs?) using a local installer\n windows_package \"Notepad++\" do\n source \"c:/installation_files/npp.5.9.2.Installer.exe\"\n action :install\n end\n\n # install VLC for that Xvid (NSIS installer)\n windows_package \"VLC media player 1.1.10\" do\n source \"http://superb-sea2.dl.sourceforge.net/project/vlc/1.1.10/win32/vlc-1.1.10-win32.exe\"\n action :install\n end\n\n # install Firefox as custom installer and manually set the silent install flags\n windows_package \"Mozilla Firefox 5.0 (x86 en-US)\" do\n source \"http://archive.mozilla.org/pub/mozilla.org/mozilla.org/firefox/releases/5.0/win32/en-US/Firefox%20Setup%205.0.exe\"\n options \"-ms\"\n installer_type :custom\n action :install\n end\n\n # Google Chrome FTW (MSI installer)\n windows_package \"Google Chrome\" do\n source \"https://dl-ssl.google.com/tag/s/appguid%3D%7B8A69D345-D564-463C-AFF1-A69D9E530F96%7D%26iid%3D%7B806F36C0-CB54-4A84-A3F3-0CF8A86575E0%7D%26lang%3Den%26browser%3D3%26usagestats%3D0%26appname%3DGoogle%2520Chrome%26needsadmin%3Dfalse/edgedl/chrome/install/GoogleChromeStandaloneEnterprise.msi\"\n action :install\n end\n\n # remove Google Chrome (but why??)\n windows_package \"Google Chrome\" do\n action :remove\n end\n\n # remove 7-Zip\n windows_package \"7-Zip 9.20 (x64 edition)\" do\n action :remove\n end\n\n\nwindows\\_printer\\_port\n----------------------\n\n**Note** Include `recipe[powershell]` on the node's expanded run list\n to ensure the powershell cookbook is downloaded to avoid circular\n dependency.\n\nCreate and delete TCP/IPv4 printer ports.\n\n### Actions\n\n- :create: Create a TCIP/IPv4 printer port. This is the default action.\n- :delete: Delete a TCIP/IPv4 printer port\n\n### Attribute Parameters\n\n- :ipv4_address: Name attribute. Required. IPv4 address, e.g. \"10.0.24.34\"\n- :port_name: Port name. Optional. Defaults to \"IP_\" + :ipv4_address\n- :port_number: Port number. Optional. Defaults to 9100.\n- :port_description: Port description. Optional.\n- :snmp_enabled: Boolean. Optional. Defaults to false.\n- :port_protocol: Port protocol, 1 (RAW), or 2 (LPR). Optional. Defaults to 1.\n\n### Examples\n\n # simplest example. Creates a TCP/IP printer port named \"IP_10.4.64.37\"\n # with all defaults\n windows_printer_port '10.4.64.37' do\n end\n\n # delete a printer port\n windows_printer_port '10.4.64.37' do\n action :delete\n end\n\n # delete a port with a custom port_name\n windows_printer_port '10.4.64.38' do\n port_name \"My awesome port\"\n action :delete\n end\n\n # Create a port with more options\n windows_printer_port '10.4.64.39' do\n port_name \"My awesome port\"\n snmp_enabled true\n port_protocol 2\n end\n\n\nwindows\\_printer\n----------------\n\n**Note** Include `recipe[powershell]` on the node's expanded run list\n to ensure the powershell cookbook is downloaded to avoid circular\n dependency.\n\nCreate Windows printer. Note that this doesn't currently install a printer\ndriver. You must already have the driver installed on the system.\n\nThe Windows Printer LWRP will automatically create a TCP/IP printer port for you using the `ipv4_address` property. If you want more granular control over the printer port, just create it using the `windows_printer_port` LWRP before creating the printer.\n\n### Actions\n\n- :create: Create a new printer\n- :delete: Delete a new printer\n\n### Attribute Parameters\n\n- :device_id: Name attribute. Required. Printer queue name, e.g. \"HP LJ 5200 in fifth floor copy room\"\n- :comment: Optional string describing the printer queue.\n- :default: Boolean. Optional. Defaults to false. Note that Windows sets the first printer defined to the default printer regardless of this setting.\n- :driver_name: String. Required. Exact name of printer driver. Note that the printer driver must already be installed on the node.\n- :location: Printer location, e.g. \"Fifth floor copy room\", or \"US/NYC/Floor42/Room4207\"\n- :shared: Boolean. Defaults to false.\n- :share_name: Printer share name.\n- :ipv4_address: Printer IPv4 address, e.g. \"10.4.64.23\". You don't have to be able to ping the IP addresss to set it. Required.\n\n\n### Examples\n\n # create a printer\n windows_printer 'HP LaserJet 5th Floor' do\n driver_name 'HP LaserJet 4100 Series PCL6'\n ipv4_address '10.4.64.38'\n end\n\n # delete a printer\n # Note: this doesn't delete the associated printer port.\n # See `windows_printer_port` above for how to delete the port.\n windows_printer 'HP LaserJet 5th Floor' do\n action :delete\n end\n\n\nwindows\\_reboot\n---------------\n\nSets required data in the node's run_state to notify `WindowsRebootHandler` a reboot is requested. If Chef run completes successfully a reboot will occur if the `WindowsRebootHandler` is properly registered as a report handler. As an action of `:request` will cause a node to reboot every Chef run, this resource is usually notified by other resources...ie restart node after a package is installed (see example below).\n\n### Actions\n\n- :request: requests a reboot at completion of successful Cher run. requires `WindowsRebootHandler` to be registered as a report handler.\n- :cancel: remove reboot request from node.run_state. this will cancel *ALL* previously requested reboots as this is a binary state.\n\n### Attribute Parameters\n\n- :timeout: Name attribute. timeout delay in seconds to wait before proceeding with the requested reboot. default is 60 seconds\n- :reason: comment on the reason for the reboot. default is 'Opscode Chef initiated reboot'\n\n### Examples\n\n # if the package installs, schedule a reboot at end of chef run\n windows_reboot 60 do\n reason 'cause chef said so'\n action :nothing\n end\n windows_package 'some_package' do\n action :install\n notifies :request, 'windows_reboot[60]'\n end\n\n # cancel the previously requested reboot\n windows_reboot 60 do\n action :cancel\n end\n\nwindows\\_registry\n-----------------\n\nCreates and modifies Windows registry keys.\n\n*Change in v1.3.0: The Win32 classes use `::Win32` to avoid namespace conflict with `Chef::Win32` (introduced in Chef 0.10.10).*\n\n### Actions\n\n- :create: create a new registry key with the provided values.\n- :modify: modify an existing registry key with the provided values.\n- :force_modify: modify an existing registry key with the provided values. ensures the value is actually set by checking multiple times. useful for fighting race conditions where two processes are trying to set the same registry key. This will be updated in the near future to use 'RegNotifyChangeKeyValue' which is exposed by the WinAPI and allows a process to register for notification on a registry key change.\n- :remove: removes a value from an existing registry key\n\n### Attribute Parameters\n\n- key_name: name attribute. The registry key to create/modify.\n- values: hash of the values to set under the registry key. The individual hash items will become respective 'Value name' => 'Value data' items in the registry key.\n- type: Type of key to create, defaults to REG_SZ. Must be a symbol, see the overview below for valid values.\n\n### Registry key types\n\n- :binary: REG_BINARY\n- :string: REG_SZ\n- :multi_string: REG_MULTI_SZ\n- :expand_string: REG_EXPAND_SZ\n- :dword: REG_DWORD\n- :dword_big_endian: REG_DWORD_BIG_ENDIAN\n- :qword: REG_QWORD\n\n### Examples\n\n # make the local windows proxy match the one set for Chef\n proxy = URI.parse(Chef::Config[:http_proxy])\n windows_registry 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' do\n values 'ProxyEnable' => 1, 'ProxyServer' => \"#{proxy.host}:#{proxy.port}\", 'ProxyOverride' => ''\n end\n\n # enable Remote Desktop and poke the firewall hole\n windows_registry 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Terminal Server' do\n values 'FdenyTSConnections' => 0\n end\n\n # Delete an item from the registry\n windows_registry 'HKCU\\Software\\Test' do\n #Key is the name of the value that you want to delete the value is always empty\n values 'ValueToDelete' => ''\n action :remove\n end\n\n # Add a REG_MULTI_SZ value to the registry\n windows_registry 'HKCU\\Software\\Test' do\n values 'MultiString' => ['line 1', 'line 2', 'line 3']\n type :multi_string\n end\n\n### Library Methods\n\n Registry.value_exists?('HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run','BGINFO')\n Registry.key_exists?('HKLM\\SOFTWARE\\Microsoft')\n BgInfo = Registry.get_value('HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run','BGINFO')\n\nwindows\\_path\n-------------\n\n### Actions\n\n- :add: Add an item to the system path\n- :remove: Remove an item from the system path\n\n### Attribute Parameters\n\n- :path: Name attribute. The name of the value to add to the system path\n\n### Examples\n\n #Add Sysinternals to the system path\n windows_path 'C:\\Sysinternals' do\n action :add\n end\n\n #Remove 7-Zip from the system path\n windows_path 'C:\\7-Zip' do\n action :remove\n end\n\nwindows\\_task\n-------------\n\nCreates, deletes or runs a Windows scheduled task. Requires Windows\nServer 2008 due to API usage.\n\n### Actions\n\n- :create: creates a task\n- :delete: deletes a task\n- :run: runs a task\n- :change: changes the un/pw or command of a task\n\n### Attribute Parameters\n\n- name: name attribute, The task name.\n- command: The command the task will run.\n- cwd: The directory the task will be run from.\n- user: The user to run the task as. (requires password)\n- password: The user's password. (requires user)\n- run_level: Run with limited or highest privileges.\n- frequency: Frequency with which to run the task. (hourly, daily, ect.)\n- frequency_modifier: Multiple for frequency. (15 minutes, 2 days)\n\n### Examples\n\n # Run Chef every 15 minutes\n windows_task \"Chef client\" do\n user \"Administrator\"\n password \"$ecR3t\"\n cwd \"C:\\chef\\bin\"\n command \"chef-client -L C:\\tmp\\\"\n run_level :highest\n frequency :minute\n frequency_modifier 15\n end\n\n # Update Chef Client task with new password and log location\n windows_task \"Chef client\" do\n user \"Administrator\"\n password \"N3wPassW0Rd\"\n cwd \"C:\\chef\\bin\"\n command \"chef-client -L C:\\chef\\logs\\\"\n action :change\n end\n\n # Delete a taks named \"old task\"\n windows_task \"old task\" do\n action :delete\n end\n\nwindows\\_zipfile\n----------------\n\nMost version of Windows do not ship with native cli utility for managing compressed files. This resource provides a pure-ruby implementation for managing zip files. Be sure to use the `not_if` or `only_if` meta parameters to guard the resource for idempotence or action will be taken on the zip file every Chef run.\n\n### Actions\n\n- :unzip: unzip a compressed file\n\n### Attribute Parameters\n\n- path: name attribute. The path where files will be unzipped to.\n- source: The source of the zip file. This can either be a URI or a local path.\n- overwrite: force an overwrite of the files if the already exists.\n- checksum: useful if source is remote, the SHA-256 checksum of the file--if the local file matches the checksum, Chef will not download it\n\n### Examples\n\n # unzip a remote zip file locally\n windows_zipfile \"c:/bin\" do\n source \"http://download.sysinternals.com/Files/SysinternalsSuite.zip\"\n action :unzip\n not_if {::File.exists?(\"c:/bin/PsExec.exe\")}\n end\n\n # unzip a local zipfile\n windows_zipfile \"c:/the_codez\" do\n source \"c:/foo/baz/the_codez.zip\"\n action :unzip\n end\n\n\nException/Report Handlers\n=========================\n\nWindowsRebootHandler\n--------------------\n\nRequired reboots are a necessary evil of configuring and managing Windows nodes. This report handler (ie fires at the end of successful Chef runs) acts on requested (Chef initiated) or pending (as determined by the OS per configuration action we performed) reboots. The `allow_pending_reboots` initialization argument should be set to false if you do not want the handler to automatically reboot a node if it has been determined a reboot is pending. Reboots can still be requested explicitly via the `windows_reboot` LWRP.\n\n## Initialization Arguments\n\n- `allow_pending_reboots`: indicator on whether the handler should act on a the Window's 'pending reboot' state. default is true\n- `timeout`: timeout delay in seconds to wait before proceeding with the reboot. default is 60 seconds\n- `reason`: comment on the reason for the reboot. default is 'Opscode Chef initiated reboot'\n\nUsage\n=====\n\nPlace an explicit dependency on this cookbook (using depends in the cookbook's metadata.rb) from any cookbook where you would like to use the Windows-specific resources/providers that ship with this cookbook.\n\n depends \"windows\"\n\ndefault\n-------\n\nConvenience recipe that installs supporting gems for many of the resources/providers that ship with this cookbook.\n\n*Change in v1.3.0: Uses chef_gem instead of gem_package to ensure gem installation in Chef 0.10.10.*\n\nreboot\\_handler\n--------------\n\nLeverages the `chef_handler` LWRP to register the `WindowsRebootHandler` report handler that ships as part of this cookbook. By default this handler is set to automatically act on pending reboots. If you would like to change this behavior override `node['windows']['allow_pending_reboots']` and set the value to false. For example:\n\n % cat roles/base.rb\n name \"base\"\n description \"base role\"\n override_attributes(\n \"windows\" => {\n \"allow_pending_reboots\" => false\n }\n )\n\nThis will still allow a reboot to be explicitly requested via the `windows_reboot` LWRP.\n\nLicense and Author\n==================\n\nAuthor:: Seth Chisamore ()\nAuthor:: Doug MacEachern ()\nAuthor:: Paul Morton ()\nAuthor:: Doug Ireton ()\n\nCopyright:: 2011, Opscode, Inc.\nCopyright:: 2010, VMware, Inc.\nCopyright:: 2011, Business Intelligence Associates, Inc\nCopyright:: 2012, Nordstrom, Inc.\n\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n", + "maintainer": "Opscode, Inc.", + "maintainer_email": "cookbooks@opscode.com", + "license": "Apache 2.0", + "platforms": { + "windows": ">= 0.0.0" + }, + "dependencies": { + "chef_handler": ">= 0.0.0" + }, + "recommendations": { + }, + "suggestions": { + }, + "conflicting": { + }, + "providing": { + }, + "replacing": { + }, + "attributes": { + }, + "groupings": { + }, + "recipes": { + }, + "version": "1.8.10" +} \ No newline at end of file diff --git a/windows/providers/auto_run.rb b/windows/providers/auto_run.rb new file mode 100644 index 0000000..36d84d6 --- /dev/null +++ b/windows/providers/auto_run.rb @@ -0,0 +1,32 @@ +# +# Author:: Paul Morotn () +# Cookbook Name:: windows +# Provider:: auto_run +# +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +action :create do + windows_registry 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' do + values new_resource.name => "\"#{new_resource.program}\" #{new_resource.args}" + end +end + +action :remove do + windows_registry 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' do + values new_resource.name => '' + action :remove + end +end \ No newline at end of file diff --git a/windows/providers/batch.rb b/windows/providers/batch.rb new file mode 100644 index 0000000..9aa347c --- /dev/null +++ b/windows/providers/batch.rb @@ -0,0 +1,62 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windws +# Provider:: batch +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +require 'tempfile' +require 'chef/resource/execute' + +action :run do + begin + script_file.puts(@new_resource.code) + script_file.close + set_owner_and_group + + # cwd hax...shell_out on windows needs to support proper 'cwd' + # follow CHEF-2357 for more + cwd = @new_resource.cwd ? "cd \"#{@new_resource.cwd}\" & " : "" + + r = Chef::Resource::Execute.new(@new_resource.name, run_context) + r.user(@new_resource.user) + r.group(@new_resource.group) + r.command("#{cwd}call \"#{script_file.path}\" #{@new_resource.flags}") + r.creates(@new_resource.creates) + r.returns(@new_resource.returns) + r.run_action(:run) + + @new_resource.updated_by_last_action(r.updated_by_last_action?) + ensure + unlink_script_file + end +end + +private +def set_owner_and_group + # FileUtils itself implements a no-op if +user+ or +group+ are nil + # You can prove this by running FileUtils.chown(nil,nil,'/tmp/file') + # as an unprivileged user. + FileUtils.chown(@new_resource.user, @new_resource.group, script_file.path) +end + +def script_file + @script_file ||= Tempfile.open(['chef-script', '.bat']) +end + +def unlink_script_file + @script_file && @script_file.close! +end diff --git a/windows/providers/feature_dism.rb b/windows/providers/feature_dism.rb new file mode 100644 index 0000000..4f24480 --- /dev/null +++ b/windows/providers/feature_dism.rb @@ -0,0 +1,47 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Provider:: feature_dism +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +include Chef::Provider::WindowsFeature::Base +include Chef::Mixin::ShellOut +include Windows::Helper + +def install_feature(name) + shell_out!("#{dism} /online /enable-feature /featurename:#{@new_resource.feature_name} /norestart", {:returns => [0,42,127]}) +end + +def remove_feature(name) + shell_out!("#{dism} /online /disable-feature /featurename:#{@new_resource.feature_name} /norestart", {:returns => [0,42,127]}) +end + +def installed? + @installed ||= begin + cmd = shell_out("#{dism} /online /Get-Features", {:returns => [0,42,127]}) + cmd.stderr.empty? && (cmd.stdout =~ /^Feature Name : #{@new_resource.feature_name}.?$\n^State : Enabled.?$/i) + end +end + +private +# account for File System Redirector +# http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx +def dism + @dism ||= begin + locate_sysnative_cmd("dism.exe") + end +end diff --git a/windows/providers/feature_servermanagercmd.rb b/windows/providers/feature_servermanagercmd.rb new file mode 100644 index 0000000..b43749b --- /dev/null +++ b/windows/providers/feature_servermanagercmd.rb @@ -0,0 +1,47 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Provider:: feature_servermanagercmd +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +include Chef::Provider::WindowsFeature::Base +include Chef::Mixin::ShellOut +include Windows::Helper + +def install_feature(name) + shell_out!("#{servermanagercmd} -install #{@new_resource.feature_name}", {:returns => [0,42,127]}) +end + +def remove_feature(name) + shell_out!("#{servermanagercmd} -remove #{@new_resource.feature_name}", {:returns => [0,42,127]}) +end + +def installed? + @installed ||= begin + cmd = shell_out("#{servermanagercmd} -query", {:returns => [0,42,127]}) + cmd.stderr.empty? && (cmd.stdout =~ /^\s*?\[X\]\s.+?\s\[#{@new_resource.feature_name}\]$/i) + end +end + +private +# account for File System Redirector +# http://msdn.microsoft.com/en-us/library/aa384187(v=vs.85).aspx +def servermanagercmd + @servermanagercmd ||= begin + locate_sysnative_cmd("servermanagercmd.exe") + end +end diff --git a/windows/providers/package.rb b/windows/providers/package.rb new file mode 100644 index 0000000..01bc860 --- /dev/null +++ b/windows/providers/package.rb @@ -0,0 +1,252 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Provider:: package +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'win32/registry' +end + +require 'chef/mixin/shell_out' +require 'chef/mixin/language' + +include Chef::Mixin::ShellOut +include Windows::Helper + +# the logic in all action methods mirror that of +# the Chef::Provider::Package which will make +# 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 @new_resource.version != nil && @new_resource.version != @current_resource.version + install_version = @new_resource.version + # If it's not installed at all, install it + elsif @current_resource.version == nil + install_version = candidate_version + end + + if install_version + Chef::Log.info("Installing #{@new_resource} version #{install_version}") + status = install_package(@new_resource.package_name, install_version) + if status + @new_resource.updated_by_last_action(true) + end + end +end + +action :upgrade do + if @current_resource.version != candidate_version + orig_version = @current_resource.version || "uninstalled" + Chef::Log.info("Upgrading #{@new_resource} version from #{orig_version} to #{candidate_version}") + status = upgrade_package(@new_resource.package_name, candidate_version) + if status + @new_resource.updated_by_last_action(true) + end + end +end + +action :remove do + if removing_package? + Chef::Log.info("Removing #{@new_resource}") + remove_package(@current_resource.package_name, @new_resource.version) + @new_resource.updated_by_last_action(true) + else + end +end + +def removing_package? + if @current_resource.version.nil? + false # nothing to remove + elsif @new_resource.version.nil? + true # remove any version of a package + elsif @new_resource.version == @current_resource.version + true # remove the version we have + else + false # we don't have the version we want to remove + end +end + +def expand_options(options) + options ? " #{options}" : "" +end + +# these methods are the required overrides of +# a provider that extends from Chef::Provider::Package +# so refactoring into core Chef should be easy + +def load_current_resource + @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) + @current_resource.package_name(@new_resource.package_name) + @current_resource.version(nil) + + unless current_installed_version.nil? + @current_resource.version(current_installed_version) + end + + @current_resource +end + +def current_installed_version + @current_installed_version ||= begin + if installed_packages.include?(@new_resource.package_name) + installed_packages[@new_resource.package_name][:version] + end + end +end + +def candidate_version + @candidate_version ||= begin + @new_resource.version || 'latest' + end +end + +def install_package(name,version) + Chef::Log.debug("Processing #{@new_resource} as a #{installer_type} installer.") + install_args = [cached_file(@new_resource.source, @new_resource.checksum), expand_options(unattended_installation_flags), expand_options(@new_resource.options)] + Chef::Log.info("Starting installation...this could take awhile.") + Chef::Log.debug "Install command: #{ sprintf(install_command_template, *install_args) }" + shell_out!(sprintf(install_command_template, *install_args), {:timeout => @new_resource.timeout, :returns => @new_resource.success_codes}) +end + +def remove_package(name, version) + uninstall_string = installed_packages[@new_resource.package_name][:uninstall_string] + Chef::Log.info("Registry provided uninstall string for #{@new_resource} is '#{uninstall_string}'") + uninstall_command = begin + if uninstall_string =~ /msiexec/i + "#{uninstall_string} /qn" + else + uninstall_string.gsub!('"','') + "start \"\" /wait /d\"#{::File.dirname(uninstall_string)}\" #{::File.basename(uninstall_string)}#{expand_options(@new_resource.options)} /S" + end + end + Chef::Log.info("Removing #{@new_resource} with uninstall command '#{uninstall_command}'") + shell_out!(uninstall_command, {:returns => @new_resource.success_codes}) +end + +private + +def install_command_template + case installer_type + when :msi + "msiexec%2$s \"%1$s\"%3$s" + else + "start \"\" /wait \"%1$s\"%2$s%3$s" + end +end + +def uninstall_command_template + case installer_type + when :msi + "msiexec %2$s %1$s" + else + "start \"\" /wait /d%1$s %2$s %3$s" + end +end + +# http://unattended.sourceforge.net/installers.php +def unattended_installation_flags + case installer_type + when :msi + # this is no-ui + "/qn /i" + when :installshield + "/s /sms" + when :nsis + "/S /NCRC" + when :inno + #"/sp- /silent /norestart" + "/verysilent /norestart" + when :wise + "/s" + else + end +end + +def installed_packages + @installed_packages || begin + installed_packages = {} + # Computer\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall + installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_LOCAL_MACHINE)) #rescue nil + # 64-bit registry view + # Computer\HKEY_LOCAL_MACHINE\Software\Wow6464Node\Microsoft\Windows\CurrentVersion\Uninstall + installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0100))) #rescue nil + # 32-bit registry view + # Computer\HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall + installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_LOCAL_MACHINE, (::Win32::Registry::Constants::KEY_READ | 0x0200))) #rescue nil + # Computer\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall + installed_packages.merge!(extract_installed_packages_from_key(::Win32::Registry::HKEY_CURRENT_USER)) #rescue nil + installed_packages + end +end + +def extract_installed_packages_from_key(hkey = ::Win32::Registry::HKEY_LOCAL_MACHINE, desired = ::Win32::Registry::Constants::KEY_READ) + uninstall_subkey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall' + packages = {} + begin + ::Win32::Registry.open(hkey, uninstall_subkey, desired) do |reg| + reg.each_key do |key, wtime| + begin + k = reg.open(key, desired) + display_name = k["DisplayName"] rescue nil + version = k["DisplayVersion"] rescue "NO VERSION" + uninstall_string = k["UninstallString"] rescue nil + if display_name + packages[display_name] = {:name => display_name, + :version => version, + :uninstall_string => uninstall_string} + end + rescue ::Win32::Registry::Error + end + end + end + rescue ::Win32::Registry::Error + end + packages +end + +def installer_type + @installer_type || begin + if @new_resource.installer_type + @new_resource.installer_type + else + basename = ::File.basename(cached_file(@new_resource.source, @new_resource.checksum)) + if basename.split(".").last.downcase == "msi" # Microsoft MSI + :msi + else + # search the binary file for installer type + contents = ::Kernel.open(::File.expand_path(cached_file(@new_resource.source)), "rb") {|io| io.read } # TODO limit data read in + case contents + when /inno/i # Inno Setup + :inno + when /wise/i # Wise InstallMaster + :wise + when /nsis/i # Nullsoft Scriptable Install System + :nsis + else + # if file is named 'setup.exe' assume installshield + if basename == "setup.exe" + :installshield + else + raise Chef::Exceptions::AttributeNotFound, "installer_type could not be determined, please set manually" + end + end + end + end + end +end diff --git a/windows/providers/pagefile.rb b/windows/providers/pagefile.rb new file mode 100644 index 0000000..e80247e --- /dev/null +++ b/windows/providers/pagefile.rb @@ -0,0 +1,153 @@ +# +# Author:: Kevin Moser () +# Cookbook Name:: windows +# Provider:: pagefile +# +# Copyright:: 2012, Nordstrom, Inc. +# +# 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. +# + +include Chef::Mixin::ShellOut +include Windows::Helper + +action :set do + pagefile = @new_resource.name + initial_size = @new_resource.initial_size + maximum_size = @new_resource.maximum_size + system_managed = @new_resource.system_managed + automatic_managed = @new_resource.automatic_managed + updated = false + + if automatic_managed + unless automatic_managed? + set_automatic_managed + updated = true + end + else + if automatic_managed? + unset_automatic_managed + updated = true + end + + # Check that the resource is not just trying to unset automatic managed, if it is do nothing more + if (initial_size && maximum_size) || system_managed + unless exists?(pagefile) + create(pagefile) + end + + if system_managed + unless max_and_min_set?(pagefile, 0, 0) + set_system_managed(pagefile) + updated = true + end + else + unless max_and_min_set?(pagefile, initial_size, maximum_size) + set_custom_size(pagefile, initial_size, maximum_size) + updated = true + end + end + end + end + + @new_resource.updated_by_last_action(updated) +end + +action :delete do + pagefile = @new_resource.name + updated = false + + if exists?(pagefile) + delete(pagefile) + updated = true + end + + @new_resource.updated_by_last_action(updated) +end + + +private +def exists?(pagefile) + @exists ||= begin + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" list /format:list", {:returns => [0]}) + cmd.stderr.empty? && (cmd.stdout =~ /SettingID=#{get_setting_id(pagefile)}/i) + end +end + +def max_and_min_set?(pagefile, min, max) + @max_and_min_set ||= begin + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" list /format:list", {:returns => [0]}) + cmd.stderr.empty? && (cmd.stdout =~ /InitialSize=#{min}/i) && (cmd.stdout =~ /MaximumSize=#{max}/i) + end +end + +def create(pagefile) + Chef::Log.debug("Creating pagefile #{pagefile}") + cmd = shell_out("#{wmic} pagefileset create name=\"#{win_friendly_path(pagefile)}\"") + check_for_errors(cmd.stderr) +end + +def delete(pagefile) + Chef::Log.debug("Removing pagefile #{pagefile}") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" delete") + check_for_errors(cmd.stderr) +end + +def automatic_managed? + @automatic_managed ||= begin + cmd = shell_out("#{wmic} computersystem where name=\"%computername%\" get AutomaticManagedPagefile /format:list") + cmd.stderr.empty? && (cmd.stdout =~ /AutomaticManagedPagefile=TRUE/i) + end +end + +def set_automatic_managed + Chef::Log.debug("Setting pagefile to Automatic Managed") + cmd = shell_out("#{wmic} computersystem where name=\"%computername%\" set AutomaticManagedPagefile=True") + check_for_errors(cmd.stderr) +end + +def unset_automatic_managed + Chef::Log.debug("Setting pagefile to User Managed") + cmd = shell_out("#{wmic} computersystem where name=\"%computername%\" set AutomaticManagedPagefile=False") + check_for_errors(cmd.stderr) +end + +def set_custom_size(pagefile, min, max) + Chef::Log.debug("Setting #{pagefile} to InitialSize=#{min} & MaximumSize=#{max}") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" set InitialSize=#{min},MaximumSize=#{max}", {:returns => [0]}) + check_for_errors(cmd.stderr) +end + +def set_system_managed(pagefile) + Chef::Log.debug("Setting #{pagefile} to System Managed") + cmd = shell_out("#{wmic} pagefileset where SettingID=\"#{get_setting_id(pagefile)}\" set InitialSize=0,MaximumSize=0", {:returns => [0]}) + check_for_errors(cmd.stderr) +end + +def get_setting_id(pagefile) + pagefile = win_friendly_path(pagefile) + pagefile = pagefile.split("\\") + "#{pagefile[1]} @ #{pagefile[0]}" +end + +def check_for_errors(stderr) + unless stderr.empty? + Chef::Log.fatal(stderr) + end +end + +def wmic + @wmic ||= begin + locate_sysnative_cmd("wmic.exe") + end +end \ No newline at end of file diff --git a/windows/providers/path.rb b/windows/providers/path.rb new file mode 100644 index 0000000..6ec9191 --- /dev/null +++ b/windows/providers/path.rb @@ -0,0 +1,35 @@ +# +# Author:: Paul Morotn () +# Cookbook Name:: windows +# Provider:: path +# +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +action :add do + env "PATH" do + action :modify + delim ::File::PATH_SEPARATOR + value new_resource.path + end +end + +action :remove do + env "PATH" do + action :delete + delim ::File::PATH_SEPARATOR + value new_resource.path + end +end \ No newline at end of file diff --git a/windows/providers/printer.rb b/windows/providers/printer.rb new file mode 100644 index 0000000..d53e287 --- /dev/null +++ b/windows/providers/printer.rb @@ -0,0 +1,100 @@ +# +# Author:: Doug Ireton () +# Cookbook Name:: windows +# Provider:: printer +# +# Copyright:: 2012, Nordstrom, Inc. +# +# 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. +# + +# Support whyrun +def whyrun_supported? + true +end + +action :create do + if @current_resource.exists + Chef::Log.info "#{ @new_resource } already exists - nothing to do." + else + converge_by("Create #{ @new_resource }") do + create_printer + end + end +end + +action :delete do + if @current_resource.exists + converge_by("Delete #{ @new_resource }") do + delete_printer + end + else + Chef::Log.info "#{ @current_resource } doesn't exist - can't delete." + end +end + +def load_current_resource + @current_resource = Chef::Resource::WindowsPrinter.new(@new_resource.name) + @current_resource.name(@new_resource.name) + + if printer_exists?(@current_resource.name) + # TODO: Set @current_resource printer properties from registry + @current_resource.exists = true + end +end + + +private + +PRINTERS_REG_KEY = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printers\\'.freeze unless defined?(PRINTERS_REG_KEY) + +def printer_exists?(name) + printer_reg_key = PRINTERS_REG_KEY + name + Chef::Log.debug "Checking to see if this reg key exists: '#{ printer_reg_key }'" + Registry.key_exists?(printer_reg_key) +end + +def create_printer + + # Create the printer port first + windows_printer_port new_resource.ipv4_address do + end + + port_name = "IP_#{ new_resource.ipv4_address }" + + powershell "Creating printer: #{ new_resource.name }" do + code <<-EOH + + Set-WmiInstance -class Win32_Printer ` + -EnableAllPrivileges ` + -Argument @{ DeviceID = "#{ new_resource.device_id }"; + Comment = "#{ new_resource.comment }"; + Default = "$#{ new_resource.default }"; + DriverName = "#{ new_resource.driver_name }"; + Location = "#{ new_resource.location }"; + PortName = "#{ port_name }"; + Shared = "$#{ new_resource.shared }"; + ShareName = "#{ new_resource.share_name }"; + } + EOH + end +end + +def delete_printer + powershell "Deleting printer: #{ new_resource.name }" do + code <<-EOH + $printer = Get-WMIObject -class Win32_Printer -EnableAllPrivileges -Filter "name = '#{ new_resource.name }'" + $printer.Delete() + EOH + end +end diff --git a/windows/providers/printer_port.rb b/windows/providers/printer_port.rb new file mode 100644 index 0000000..ab88247 --- /dev/null +++ b/windows/providers/printer_port.rb @@ -0,0 +1,102 @@ +# +# Author:: Doug Ireton () +# Cookbook Name:: windows +# Provider:: printer_port +# +# Copyright:: 2012, Nordstrom, Inc. +# +# 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. +# + +# Support whyrun +def whyrun_supported? + true +end + +action :create do + if @current_resource.exists + Chef::Log.info "#{ @new_resource } already exists - nothing to do." + else + converge_by("Create #{ @new_resource }") do + create_printer_port + end + end +end + +action :delete do + if @current_resource.exists + converge_by("Delete #{ @new_resource }") do + delete_printer_port + end + else + Chef::Log.info "#{ @current_resource } doesn't exist - can't delete." + end +end + +def load_current_resource + @current_resource = Chef::Resource::WindowsPrinterPort.new(@new_resource.name) + @current_resource.name(@new_resource.name) + @current_resource.ipv4_address(@new_resource.ipv4_address) + @current_resource.port_name(@new_resource.port_name || "IP_#{ @new_resource.ipv4_address }") + + if port_exists?(@current_resource.port_name) + # TODO: Set @current_resource port properties from registry + @current_resource.exists = true + end +end + + +private + +PORTS_REG_KEY = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\Standard TCP/IP Port\Ports\\'.freeze unless defined?(PORTS_REG_KEY) + +def port_exists?(name) + port_reg_key = PORTS_REG_KEY + name + + Chef::Log.debug "Checking to see if this reg key exists: '#{ port_reg_key }'" + Registry.key_exists?(port_reg_key) +end + + +def create_printer_port + + port_name = new_resource.port_name || "IP_#{ new_resource.ipv4_address }" + + # create the printer port using PowerShell + powershell "Creating printer port #{ new_resource.port_name }" do + code <<-EOH + + Set-WmiInstance -class Win32_TCPIPPrinterPort ` + -EnableAllPrivileges ` + -Argument @{ HostAddress = "#{ new_resource.ipv4_address }"; + Name = "#{ port_name }"; + Description = "#{ new_resource.port_description }"; + PortNumber = "#{ new_resource.port_number }"; + Protocol = "#{ new_resource.port_protocol }"; + SNMPEnabled = "$#{ new_resource.snmp_enabled }"; + } + EOH + end +end + +def delete_printer_port + + port_name = new_resource.port_name || "IP_#{ new_resource.ipv4_address }" + + powershell "Deleting printer port: #{ new_resource.port_name }" do + code <<-EOH + $port = Get-WMIObject -class Win32_TCPIPPrinterPort -EnableAllPrivileges -Filter "name = '#{ port_name }'" + $port.Delete() + EOH + end +end diff --git a/windows/providers/reboot.rb b/windows/providers/reboot.rb new file mode 100644 index 0000000..4fc5032 --- /dev/null +++ b/windows/providers/reboot.rb @@ -0,0 +1,31 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Provider:: reboot +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +action :request do + node.run_state[:reboot_requested] = true + node.run_state[:reboot_timeout] = @new_resource.timeout + node.run_state[:reboot_reason] = @new_resource.reason +end + +action :cancel do + node.run_state.delete(:reboot_requested) + node.run_state.delete(:reboot_timeout) + node.run_state.delete(:reboot_reason) +end diff --git a/windows/providers/registry.rb b/windows/providers/registry.rb new file mode 100644 index 0000000..afd03ff --- /dev/null +++ b/windows/providers/registry.rb @@ -0,0 +1,72 @@ +# +# Author:: Doug MacEachern () +# Author:: Seth Chisamore () +# Author:: Paul Morton () +# Cookbook Name:: windows +# Provider:: registry +# +# Copyright:: 2010, VMware, Inc. +# Copyright:: 2011, Opscode, Inc. +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +include Windows::RegistryHelper + +action :create do + registry_update(:create) +end + +action :modify do + registry_update(:open) +end + +action :force_modify do + require 'timeout' + Timeout.timeout(120) do + @new_resource.values.each do |value_name, value_data| + i = 1 + until i > 5 do + desired_value_data = value_data + current_value_data = get_value(@new_resource.key_name.dup, value_name.dup) + if current_value_data.to_s == desired_value_data.to_s + Chef::Log.debug("#{@new_resource} value [#{value_name}] desired [#{desired_value_data}] data already set. Check #{i}/5.") + i+=1 + else + Chef::Log.debug("#{@new_resource} value [#{value_name}] current [#{current_value_data}] data not equal to desired [#{desired_value_data}] data. Setting value and restarting check loop.") + begin + registry_update(:open) + rescue Exception + registry_update(:create) + end + i=0 # start count loop over + end + end + end + break + end +end + +action :remove do + delete_value(@new_resource.key_name,@new_resource.values) +end + +private +def registry_update(mode) + + Chef::Log.debug("Registry Mode (#{mode})") + updated = set_value(mode,@new_resource.key_name,@new_resource.values,@new_resource.type) + @new_resource.updated_by_last_action(updated) + +end diff --git a/windows/providers/shortcut.rb b/windows/providers/shortcut.rb new file mode 100644 index 0000000..6913914 --- /dev/null +++ b/windows/providers/shortcut.rb @@ -0,0 +1,56 @@ +# +# Author:: Doug MacEachern +# Cookbook Name:: windows +# Provider:: shortcut +# +# Copyright:: 2010, VMware, Inc. +# +# 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. +# + +def load_current_resource + require 'win32ole' + + @link = WIN32OLE.new("WScript.Shell").CreateShortcut(@new_resource.name) + + @current_resource = Chef::Resource::WindowsShortcut.new(@new_resource.name) + @current_resource.name(@new_resource.name) + @current_resource.target(@link.TargetPath) + @current_resource.arguments(@link.Arguments) + @current_resource.description(@link.Description) + @current_resource.cwd(@link.WorkingDirectory) +end + +# Check to see if the shorcut needs any changes +# +# === Returns +# :: If a change is required +# :: If the shorcuts are identical +def compare_shortcut + [:target, :arguments, :description, :cwd].any? do |attr| + !@new_resource.send(attr).nil? && @current_resource.send(attr) != @new_resource.send(attr) + end +end + +def action_create + if compare_shortcut + @link.TargetPath = @new_resource.target if @new_resource.target != nil + @link.Arguments = @new_resource.arguments if @new_resource.arguments != nil + @link.Description = @new_resource.description if @new_resource.description != nil + @link.WorkingDirectory = @new_resource.cwd if @new_resource.cwd != nil + #ignoring: WindowStyle, Hotkey, IconLocation + @link.Save + Chef::Log.info("Added #{@new_resource} shortcut") + @updated = true + end +end diff --git a/windows/providers/task.rb b/windows/providers/task.rb new file mode 100644 index 0000000..f9c5e96 --- /dev/null +++ b/windows/providers/task.rb @@ -0,0 +1,124 @@ +# +# Author:: Paul Mooring () +# Cookbook Name:: windows +# Provider:: task +# +# Copyright:: 2012, Opscode, Inc. +# +# 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. +# + +require 'chef/mixin/shell_out' +include Chef::Mixin::ShellOut + +action :create do + if @current_resource.exists + Chef::Log.info "#{@new_resource} task already exists - nothing to do" + else + cmd = "schtasks /Create /TN \"#{@new_resource.name}\" " + cmd += "/SC #{@new_resource.frequency} /MO #{@new_resource.frequency_modifier} " + cmd += "/TR \"#{@new_resource.command}\" " + if @new_resource.user && @new_resource.password + cmd += "/RU \"#{@new_resource.user}\" /RP \"#{@new_resource.password}\" " + elsif (@new_resource.user and !@new_resource.password) || (@new_resource.password and !@new_resource.user) + Chef::Log.fatal "#{@new_resource.name}: Can't specify user or password without both!" + end + cmd += "/RL HIGHEST " if @new_resource.run_level == :highest + shell_out!(cmd, {:returns => [0]}) + @new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task created" + end +end + +action :run do + if @current_resource.exists + if @current_resource.status == :running + Chef::Log.info "#{@new_resource} task is currently running, skipping run" + else + cmd = "schtasks /Run /TN \"#{@current_resource.name}\"" + shell_out!(cmd, {:returns => [0]}) + @new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task ran" + end + else + Chef::Log.debug "#{@new_resource} task doesn't exists - nothing to do" + end +end + +action :change do + if @current_resource.exists + cmd = "schtasks /Change /TN \"#{@current_resource.name}\" " + cmd += "/TR \"#{@new_resource.command}\" " if @new_resource.command + if @new_resource.user && @new_resource.password + cmd += "/RU \"#{@new_resource.user}\" /RP \"#{@new_resource.password}\" " + elsif (@new_resource.user and !@new_resource.password) || (@new_resource.password and !@new_resource.user) + Chef::Log.fatal "#{@new_resource.name}: Can't specify user or password without both!" + end + shell_out!(cmd, {:returns => [0]}) + @new_resource.updated_by_last_action true + Chef::Log.info "Change #{@new_resource} task ran" + else + Chef::Log.debug "#{@new_resource} task doesn't exists - nothing to do" + end +end + +action :delete do + if @current_resource.exists + cmd = "schtasks /Delete /TN \"#{@current_resource.name}\"" + shell_out!(cmd, {:returns => [0]}) + @new_resource.updated_by_last_action true + Chef::Log.info "#{@new_resource} task deleted" + else + Chef::Log.debug "#{@new_resource} task doesn't exists - nothing to do" + end +end + +def load_current_resource + @current_resource = Chef::Resource::WindowsTask.new(@new_resource.name) + @current_resource.name(@new_resource.name) + + task_hash = load_task_hash(@current_resource.name) + if task_hash[:TaskName] == '\\' + @new_resource.name + @current_resource.exists = true + if task_hash[:Status] == "Running" + @current_resource.status = :running + end + @current_resource.cwd(task_hash[:Folder]) + @current_resource.command(task_hash[:TaskToRun]) + @current_resource.user(task_hash[:RunAsUser]) + end if task_hash.respond_to? :[] +end + +private + +def load_task_hash(task_name) + Chef::Log.debug "looking for existing tasks" + output = `schtasks /Query /FO LIST /V /TN \"#{task_name}\" 2> NUL` + if output.empty? + task = false + else + task = Hash.new + + output.split("\n").map! do |line| + line.split(":", 2).map! do |field| + field.strip + end + end.each do |field| + if field.kind_of? Array and field[0].respond_to? :to_sym + task[field[0].gsub(/\s+/,"").to_sym] = field[1] + end + end + end + + task +end diff --git a/windows/providers/zipfile.rb b/windows/providers/zipfile.rb new file mode 100644 index 0000000..44b3731 --- /dev/null +++ b/windows/providers/zipfile.rb @@ -0,0 +1,91 @@ +# +# Author:: Doug MacEachern () +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Provider:: unzip +# +# Copyright:: 2010, VMware, Inc. +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +include Windows::Helper + +require 'find' + +action :unzip do + ensure_rubyzip_gem_installed + Chef::Log.debug("unzip #{@new_resource.source} => #{@new_resource.path} (overwrite=#{@new_resource.overwrite})") + + Zip::ZipFile.open(cached_file(@new_resource.source, @new_resource.checksum)) do |zip| + zip.each do |entry| + path = ::File.join(@new_resource.path, entry.name) + FileUtils.mkdir_p(::File.dirname(path)) + if @new_resource.overwrite && ::File.exists?(path) && !::File.directory?(path) + FileUtils.rm(path) + end + zip.extract(entry, path) + end + end + @new_resource.updated_by_last_action(true) +end + +action :zip do + ensure_rubyzip_gem_installed + # sanitize paths for windows. + @new_resource.source.downcase.gsub!(::File::SEPARATOR, ::File::ALT_SEPARATOR) + @new_resource.path.downcase.gsub!(::File::SEPARATOR, ::File::ALT_SEPARATOR) + Chef::Log.debug("zip #{@new_resource.source} => #{@new_resource.path} (overwrite=#{@new_resource.overwrite})") + + if @new_resource.overwrite == false && ::File.exists?(@new_resource.path) + Chef::Log.info("file #{@new_resource.path} already exists and overwrite is set to false, exiting") + else + # delete the archive if it already exists, because we are recreating it. + if ::File.exists?(@new_resource.path) + ::File.unlink(@new_resource.path) + end + # only supporting compression of a single directory (recursively). + if ::File.directory?(@new_resource.source) + z = Zip::ZipFile.new(@new_resource.path, true) + unless @new_resource.source =~ /::File::ALT_SEPARATOR$/ + @new_resource.source << ::File::ALT_SEPARATOR + end + Find.find(@new_resource.source) do |f| + f.downcase.gsub!(::File::SEPARATOR, ::File::ALT_SEPARATOR) + # don't add root directory to the zipfile. + next if f == @new_resource.source + # strip the root directory from the filename before adding it to the zipfile. + zip_fname = f.sub(@new_resource.source, '') + Chef::Log.debug("adding #{zip_fname} to archive, sourcefile is: #{f}") + z.add(zip_fname, f) + end + z.close + else + Chef::Log.info("Single directory must be specified for compression, and #{@new_resource.source} does not meet that criteria.") + end + end +end + +private +def ensure_rubyzip_gem_installed + begin + require 'zip/zip' + rescue LoadError + Chef::Log.info("Missing gem 'rubyzip'...installing now.") + chef_gem "rubyzip" do + version node['windows']['rubyzipversion'] + end + require 'zip/zip' + end +end diff --git a/windows/recipes/default.rb b/windows/recipes/default.rb new file mode 100644 index 0000000..f0dbffb --- /dev/null +++ b/windows/recipes/default.rb @@ -0,0 +1,34 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Recipe:: default +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +# gems with precompiled binaries +%w{ win32-api win32-service }.each do |win_gem| + chef_gem win_gem do + options '--platform=mswin32' + action :install + end +end + +# the rest +%w{ windows-api windows-pr win32-dir win32-event win32-mutex }.each do |win_gem| + chef_gem win_gem do + action :install + end +end diff --git a/windows/recipes/reboot_handler.rb b/windows/recipes/reboot_handler.rb new file mode 100644 index 0000000..2e55b91 --- /dev/null +++ b/windows/recipes/reboot_handler.rb @@ -0,0 +1,32 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Recipe:: restart_handler +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +remote_directory node['chef_handler']['handler_path'] do + source 'handlers' + recursive true + action :create +end + +chef_handler 'WindowsRebootHandler' do + source "#{node['chef_handler']['handler_path']}/windows_reboot_handler.rb" + arguments node['windows']['allow_pending_reboots'] + supports :report => true, :exception => false + action :enable +end diff --git a/windows/resources/auto_run.rb b/windows/resources/auto_run.rb new file mode 100644 index 0000000..7beecc5 --- /dev/null +++ b/windows/resources/auto_run.rb @@ -0,0 +1,30 @@ +# +# Author:: Paul Morotn () +# Cookbook Name:: windows +# Resource:: auto_run +# +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +def initialize(name,run_context=nil) + super + @action = :create +end + +actions :create, :remove + +attribute :program, :kind_of => String +attribute :name, :kind_of => String, :name_attribute => true +attribute :args, :kind_of => String, :default => '' diff --git a/windows/resources/batch.rb b/windows/resources/batch.rb new file mode 100644 index 0000000..7d4e917 --- /dev/null +++ b/windows/resources/batch.rb @@ -0,0 +1,36 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Resource:: batch +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +actions :run + +attribute :command, :kind_of => String, :name_attribute => true +attribute :cwd, :kind_of => String, :default => nil +attribute :code, :kind_of => String, :default => nil +attribute :user, :kind_of => [ String, Integer ], :default => nil +attribute :group, :kind_of => [ String, Integer ], :default => nil +attribute :creates, :kind_of => [ String ], :default => nil +attribute :flags, :kind_of => [ String ], :default => nil +attribute :returns, :kind_of => [Integer, Array], :default => 0 + +def initialize(name, run_context=nil) + super + @action = :run + @command = name +end diff --git a/windows/resources/feature.rb b/windows/resources/feature.rb new file mode 100644 index 0000000..b67c0fb --- /dev/null +++ b/windows/resources/feature.rb @@ -0,0 +1,40 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Resource:: feature +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +include Windows::Helper + +actions :install, :remove + +attribute :feature_name, :kind_of => String, :name_attribute => true + +def initialize(name, run_context=nil) + super + @action = :install + @provider = lookup_provider_constant(locate_default_provider) +end + +private +def locate_default_provider + if ::File.exists?(locate_sysnative_cmd('dism.exe')) + :windows_feature_dism + elsif ::File.exists?(locate_sysnative_cmd('servermanagercmd.exe')) + :windows_feature_servermanagercmd + end +end \ No newline at end of file diff --git a/windows/resources/package.rb b/windows/resources/package.rb new file mode 100644 index 0000000..a9e822e --- /dev/null +++ b/windows/resources/package.rb @@ -0,0 +1,46 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Resource:: package +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +actions :install, :remove + +default_action :install + +attribute :package_name, :kind_of => String, :name_attribute => true +attribute :source, :kind_of => String, :required => true +attribute :version, :kind_of => String +attribute :options, :kind_of => String +attribute :installer_type, :kind_of => Symbol, :default => nil, :equal_to => [:msi, :inno, :nsis, :wise, :installshield, :custom] +attribute :checksum, :kind_of => String +attribute :timeout, :kind_of => Integer, :default => 600 +attribute :success_codes, :kind_of => Array, :default => [0, 42, 127] + +# TODO + +# add preseeding support +#attribute :response_file + +# allow target dirtory of installation to be set +#attribute :target_dir + +# Covers 0.10.8 and earlier +def initialize(*args) + super + @action = :install +end diff --git a/windows/resources/pagefile.rb b/windows/resources/pagefile.rb new file mode 100644 index 0000000..3d95f13 --- /dev/null +++ b/windows/resources/pagefile.rb @@ -0,0 +1,29 @@ +# +# Author:: Kevin Moser () +# Cookbook Name:: windows +# Resource:: pagefile +# +# Copyright:: 2012, Nordstrom, Inc. +# +# 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. +# + +actions :set, :delete + +attribute :name, :kind_of => String, :name_attribute => true +attribute :system_managed, :kind_of => [TrueClass, FalseClass] +attribute :automatic_managed, :kind_of => [TrueClass, FalseClass], :default => false +attribute :initial_size, :kind_of => Integer +attribute :maximum_size, :kind_of => Integer + +default_action :set \ No newline at end of file diff --git a/windows/resources/path.rb b/windows/resources/path.rb new file mode 100644 index 0000000..f39aa8e --- /dev/null +++ b/windows/resources/path.rb @@ -0,0 +1,28 @@ +# +# Author:: Paul Morotn () +# Cookbook Name:: windows +# Resource:: path +# +# Copyright:: 2011, Business Intelligence Associates, Inc +# +# 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. +# + +def initialize(name,run_context=nil) + super + @action = :add +end + +actions :add, :remove + +attribute :path, :kind_of => String, :name_attribute => true diff --git a/windows/resources/printer.rb b/windows/resources/printer.rb new file mode 100644 index 0000000..5effa33 --- /dev/null +++ b/windows/resources/printer.rb @@ -0,0 +1,41 @@ +# +# Author:: Doug Ireton () +# Cookbook Name:: windows +# Resource:: printer +# +# Copyright:: 2012, Nordstrom, Inc. +# +# 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. +# +# See here for more info: +# http://msdn.microsoft.com/en-us/library/windows/desktop/aa394492(v=vs.85).aspx + +require 'resolv' + +actions :create, :delete + +default_action :create + +attribute :device_id, :kind_of => String, :name_attribute => true, + :required => true +attribute :comment, :kind_of => String + +attribute :default, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :driver_name, :kind_of => String, :required => true +attribute :location, :kind_of => String +attribute :shared, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :share_name, :kind_of => String + +attribute :ipv4_address, :kind_of => String, :regex => Resolv::IPv4::Regex + +attr_accessor :exists diff --git a/windows/resources/printer_port.rb b/windows/resources/printer_port.rb new file mode 100644 index 0000000..b79a6fc --- /dev/null +++ b/windows/resources/printer_port.rb @@ -0,0 +1,40 @@ +# +# Author:: Doug Ireton () +# Cookbook Name:: windows +# Resource:: printer_port +# +# Copyright:: 2012, Nordstrom, Inc. +# +# 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. +# +# See here for more info: +# http://msdn.microsoft.com/en-us/library/windows/desktop/aa394492(v=vs.85).aspx + +require 'resolv' + +actions :create, :delete + +default_action :create + +attribute :ipv4_address, :name_attribute => true, :kind_of => String, + :required => true, :regex => Resolv::IPv4::Regex + +attribute :port_name , :kind_of => String +attribute :port_number , :kind_of => Fixnum, :default => 9100 +attribute :port_description, :kind_of => String +attribute :snmp_enabled , :kind_of => [ TrueClass, FalseClass ], + :default => false + +attribute :port_protocol, :kind_of => Fixnum, :default => 1, :equal_to => [1, 2] + +attr_accessor :exists diff --git a/windows/resources/reboot.rb b/windows/resources/reboot.rb new file mode 100644 index 0000000..c788927 --- /dev/null +++ b/windows/resources/reboot.rb @@ -0,0 +1,29 @@ +# +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Resource:: reboot +# +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +actions :request, :cancel + +attribute :timeout, :kind_of => Integer, :default => 60, :name_attribute => true +attribute :reason, :kind_of => String, :default => '' + +def initialize(name,run_context=nil) + super + @action = :request +end diff --git a/windows/resources/registry.rb b/windows/resources/registry.rb new file mode 100644 index 0000000..1289dbf --- /dev/null +++ b/windows/resources/registry.rb @@ -0,0 +1,33 @@ +# +# Author:: Doug MacEachern () +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Resource:: registry +# +# Copyright:: 2010, VMware, Inc. +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +actions :create, :modify, :force_modify, :remove + +attribute :key_name, :kind_of => String, :name_attribute => true +attribute :values, :kind_of => Hash +attribute :type, :kind_of => Symbol, :default => nil, :equal_to => [:binary, :string, :multi_string, :expand_string, :dword, :dword_big_endian, :qword] + +def initialize(name, run_context=nil) + super + @action = :modify + @key_name = name +end diff --git a/windows/resources/shortcut.rb b/windows/resources/shortcut.rb new file mode 100644 index 0000000..eb6268b --- /dev/null +++ b/windows/resources/shortcut.rb @@ -0,0 +1,35 @@ +# +# Author:: Doug MacEachern +# Cookbook Name:: windows +# Resource:: shortcut +# +# Copyright:: 2010, VMware, Inc. +# +# 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. +# + +actions :create + +default_action :create + +attribute :name, :kind_of => String +attribute :target, :kind_of => String +attribute :arguments, :kind_of => String +attribute :description, :kind_of => String +attribute :cwd, :kind_of => String + +# Covers 0.10.8 and earlier +def initialize(*args) + super + @action = :create +end diff --git a/windows/resources/task.rb b/windows/resources/task.rb new file mode 100644 index 0000000..d6a1c69 --- /dev/null +++ b/windows/resources/task.rb @@ -0,0 +1,46 @@ +# +# Author:: Paul Mooring () +# Cookbook Name:: windows +# Resource:: task +# +# Copyright:: 2012, Opscode, Inc. +# +# 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. +# + +# Passwords can't be loaded for existing tasks, making :modify both confusing +# and not very useful +actions :create, :delete, :run, :change + +attribute :name, :kind_of => String, :name_attribute => true +attribute :command, :kind_of => String +attribute :cwd, :kind_of => String +attribute :user, :kind_of => String, :default => nil +attribute :password, :kind_of => String, :default => nil +attribute :run_level, :equal_to => [:highest, :limited], :default => :limited +attribute :frequency_modifier, :kind_of => Integer, :default => 1 +attribute :frequency, :equal_to => [:minute, + :hourly, + :daily, + :weekly, + :monthly, + :once, + :on_logon, + :on_idle], :default => :hourly + +attr_accessor :exists, :status + +def initialize(name, run_context=nil) + super + @action = :create +end diff --git a/windows/resources/zipfile.rb b/windows/resources/zipfile.rb new file mode 100644 index 0000000..0265816 --- /dev/null +++ b/windows/resources/zipfile.rb @@ -0,0 +1,33 @@ +# +# Author:: Doug MacEachern () +# Author:: Seth Chisamore () +# Cookbook Name:: windows +# Resource:: unzip +# +# Copyright:: 2010, VMware, Inc. +# Copyright:: 2011, Opscode, Inc. +# +# 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. +# + +actions :unzip, :zip + +attribute :path, :kind_of => String, :name_attribute => true +attribute :source, :kind_of => String +attribute :overwrite, :kind_of => [ TrueClass, FalseClass ], :default => false +attribute :checksum, :kind_of => String + +def initialize(name, run_context=nil) + super + @action = :unzip +end