├── .gitignore ├── Rakefile ├── Gemfile ├── lib ├── puppet │ └── parser │ │ └── functions │ │ └── consul_info.rb └── hiera │ └── backend │ └── consul_backend.rb ├── metadata.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | Gemfile.lock 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/rake_tasks' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | 4 | gem "rake" 5 | gem "base64" 6 | gem "net/http" 7 | gem "net/https" 8 | gem "json", '>=1.1.1' 9 | gem "puppet", ENV['PUPPET_VERSION'] || '~> 3.2.0' 10 | gem "puppetlabs_spec_helper" 11 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/consul_info.rb: -------------------------------------------------------------------------------- 1 | module Puppet::Parser::Functions 2 | newfunction(:consul_info, :type => :rvalue, :doc => <<-EOS 3 | Parse the incoming consul info and return a value 4 | EOS 5 | ) do |args| 6 | 7 | data = args[0] 8 | field = args[1] 9 | if args[2] 10 | separator = args[2] 11 | else 12 | separator = ":" 13 | end 14 | debug("consul-info() :: Determined that my separator is \"#{separator}\"") 15 | 16 | if field.is_a?(Array) 17 | field_iterator = field 18 | debug("consul-info() :: Field is an Array, importing as it is #{field_iterator}") 19 | elsif field.is_a?(String) 20 | field_iterator = [] 21 | field_iterator.push(field) 22 | debug("consul-info() :: Field is a text string, converting to array #{field_iterator}") 23 | elsif field.is_a?(Hash) 24 | raise(Puppet::ParseError, 'consul_info() does not accept a hash as your field argument') 25 | end 26 | 27 | if data.is_a?(Hash) 28 | myendstring = "" 29 | debug ("consul-info() :: Data is a hash") 30 | field_iterator.each do |myfield| 31 | myendstring << "#{data[myfield]}#{separator}" 32 | end 33 | myreturn = myendstring.gsub(/#{Regexp.escape(separator)}$/, '') 34 | elsif data.is_a?(Array) 35 | debug ("consul_info() :: Data is an array") 36 | myreturn = [] 37 | data.each do |mydata| 38 | myendstring = "" 39 | field_iterator.each do |myfield| 40 | myendstring << "#{mydata[myfield]}#{separator}" 41 | end 42 | myreturn << myendstring.gsub(/#{Regexp.escape(separator)}$/, '') 43 | end 44 | else 45 | raise(Puppet::ParseError, "consul_info() does not know how to treat data #{data}") 46 | end 47 | 48 | debug("consul_info() returning #{myreturn}") 49 | return myreturn 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lynxman-hiera_consul", 3 | "version": "0.1.2", 4 | "author": "Marc Cluet", 5 | "license": "Apache-2.0", 6 | "summary": "Module for using consul as a hiera backend", 7 | "description": "Use consul by Hashicorp, as a hiera backend", 8 | "source": "https://github.com/lynxman/hiera-consul.git", 9 | "project_page": "https://forge.puppetlabs.com/lynxman/hiera_consul", 10 | "issues_url": "https://github.com/lynxman/hiera-consul/issues", 11 | "tags": ["consul", "hiera"], 12 | "operatingsystem_support": [ 13 | { 14 | "operatingsystem": "RedHat", 15 | "operatingsystemrelease": [ 16 | "5", 17 | "6", 18 | "7" 19 | ] 20 | }, 21 | { 22 | "operatingsystem": "CentOS", 23 | "operatingsystemrelease": [ 24 | "5", 25 | "6", 26 | "7" 27 | ] 28 | }, 29 | { 30 | "operatingsystem": "OracleLinux", 31 | "operatingsystemrelease": [ 32 | "5", 33 | "6", 34 | "7" 35 | ] 36 | }, 37 | { 38 | "operatingsystem": "Scientific", 39 | "operatingsystemrelease": [ 40 | "5", 41 | "6", 42 | "7" 43 | ] 44 | }, 45 | { 46 | "operatingsystem": "SLES", 47 | "operatingsystemrelease": [ 48 | "10 SP4", 49 | "11 SP1", 50 | "12" 51 | ] 52 | }, 53 | { 54 | "operatingsystem": "Debian", 55 | "operatingsystemrelease": [ 56 | "6", 57 | "7" 58 | ] 59 | }, 60 | { 61 | "operatingsystem": "Ubuntu", 62 | "operatingsystemrelease": [ 63 | "10.04", 64 | "12.04", 65 | "14.04" 66 | ] 67 | }, 68 | { 69 | "operatingsystem": "Solaris", 70 | "operatingsystemrelease": [ 71 | "10", 72 | "11" 73 | ] 74 | }, 75 | { 76 | "operatingsystem": "AIX", 77 | "operatingsystemrelease": [ 78 | "6.1", 79 | "7.1" 80 | ] 81 | } 82 | ], 83 | "dependencies": [ 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Puppet Forge](http://img.shields.io/puppetforge/v/lynxman/hiera_consul.svg)](https://forge.puppetlabs.com/lynxman/hiera_consul) 2 | 3 | [consul](http://www.consul.io) is an orchestration mechanism with fault-tolerance based on the gossip protocol and a key/value store that is strongly consistent. Hiera-consul will allow hiera to write to the k/v store for metadata centralisation and harmonisation. 4 | 5 | ## Installation 6 | 7 | For usage with puppet, install the module in your local environment, e.g.: 8 | 9 | puppet module install lynxman/hiera_consul 10 | 11 | or using a Puppetfile: 12 | 13 | mod 'lynxman/hiera_consul' 14 | 15 | Ensure the backend `consul_backend.rb` is available into your hiera environment. Depending on your hiera/puppet environment, you may need to install the backend manually (or with puppet) at the correct path, which may be puppets local ruby path, e.g. `$PUPPET_DIR/lib/ruby/vendor_ruby/hiera/backend/consul_backend.rb` 16 | 17 | Puppet loads backends differently in some version, see [#SERVER-571](https://tickets.puppetlabs.com/si/jira.issueviews:issue-html/SERVER-571/SERVER-571.html) for more information. 18 | 19 | ## Configuration 20 | 21 | The following hiera.yaml should get you started: 22 | 23 | :backends: 24 | - consul 25 | 26 | :consul: 27 | :host: 127.0.0.1 28 | :port: 8500 29 | :paths: 30 | - /v1/kv/configuration/%{fqdn} 31 | - /v1/kv/configuration/common 32 | 33 | The array `:paths:` allows hiera to access the namespaces in it. As an example, you can query `/v1/kv/configuration/common/yourkey` using 34 | 35 | hiera('yourkey', []) 36 | 37 | This will return a consul array, which can further processed. See the helper function `consul_info` below for more information. 38 | 39 | ## Extra parameters 40 | 41 | As this module uses http to talk with Consul API the following parameters are also valid and available 42 | 43 | :consul: 44 | :host: 127.0.0.1 45 | :port: 8500 46 | :use_ssl: false 47 | :ssl_verify: false 48 | :ssl_cert: /path/to/cert 49 | :ssl_key: /path/to/key 50 | :ssl_ca_cert: /path/to/ca/cert 51 | :failure: graceful 52 | :ignore_404: true 53 | :token: acl-uuid-token 54 | 55 | ## Query the catalog 56 | 57 | You can also query the Consul catalog for values by adding catalog resources in your paths, the values will be returned as an array so you will need to parse accordingly. 58 | 59 | :backends: 60 | - consul 61 | 62 | :consul: 63 | :host: 127.0.0.1 64 | :port: 8500 65 | :paths: 66 | - /v1/kv/configuration/%{fqdn} 67 | - /v1/kv/configuration/common 68 | - /v1/catalog/service 69 | - /v1/catalog/node 70 | 71 | ## Helper function 72 | 73 | ### consul_info 74 | 75 | This function will allow you to read information out of a consul Array returned by hiera, as an example here we recover node IPs based on a service: 76 | 77 | $consul_service_array = hiera('rabbitmq',[]) 78 | $mq_cluster_nodes = consul_info($consul_service_array, 'Address') 79 | 80 | In this example `$mq_cluster_nodes` will have an array with all the IP addresses related to that service 81 | 82 | You can also call it more with than one field and a separator and it will generate a composed string for each element in the consul query result. 83 | 84 | $consul_service_array = hiera('rabbitmq',[]) 85 | $mq_cluster_nodes = consul_info($consul_service_array, [ 'Address', 'Port' ], ':') 86 | 87 | The result will return an array like this: [ AddressA:PortA, AddressB:PortB ] 88 | 89 | If you want to flatten the output array you can always use [join](https://forge.puppetlabs.com/puppetlabs/stdlib) from the Puppet stdlib. 90 | 91 | $myresult = join($mq_cluster_nodes, ",") 92 | 93 | ## Thanks 94 | 95 | Heavily based on [etcd-hiera](https://github.com/garethr/hiera-etcd) written by @garethr which was inspired by [hiera-http](https://github.com/crayfishx/hiera-http) from @crayfishx. 96 | 97 | Thanks to @mitchellh for writing such wonderful tools and the [API Documentation](http://www.consul.io/docs/agent/http.html) 98 | 99 | Thanks for their contributions to [Wei Tie](https://github.com/TieWei), [Derek Tracy](https://github.com/tracyde), [Michael Chapman](https://github.com/michaeltchapman), [Kyle O'Donnell](https://github.com/kyleodonnell), [AJ](https://github.com/aj-jester) and [lcrisci](https://github.com/lcrisci) 100 | -------------------------------------------------------------------------------- /lib/hiera/backend/consul_backend.rb: -------------------------------------------------------------------------------- 1 | # Hiera backend for Consul 2 | class Hiera 3 | module Backend 4 | class Consul_backend 5 | 6 | def initialize() 7 | require 'net/http' 8 | require 'net/https' 9 | require 'json' 10 | @config = Config[:consul] 11 | if (@config[:host] && @config[:port]) 12 | @consul = Net::HTTP.new(@config[:host], @config[:port]) 13 | else 14 | raise "[hiera-consul]: Missing minimum configuration, please check hiera.yaml" 15 | end 16 | 17 | @consul.read_timeout = @config[:http_read_timeout] || 10 18 | @consul.open_timeout = @config[:http_connect_timeout] || 10 19 | @cache = {} 20 | 21 | if @config[:use_ssl] 22 | @consul.use_ssl = true 23 | 24 | if @config[:ssl_verify] == false 25 | @consul.verify_mode = OpenSSL::SSL::VERIFY_NONE 26 | else 27 | @consul.verify_mode = OpenSSL::SSL::VERIFY_PEER 28 | end 29 | 30 | if @config[:ssl_cert] 31 | store = OpenSSL::X509::Store.new 32 | store.add_cert(OpenSSL::X509::Certificate.new(File.read(@config[:ssl_ca_cert]))) 33 | @consul.cert_store = store 34 | 35 | @consul.key = OpenSSL::PKey::RSA.new(File.read(@config[:ssl_key])) 36 | @consul.cert = OpenSSL::X509::Certificate.new(File.read(@config[:ssl_cert])) 37 | end 38 | else 39 | @consul.use_ssl = false 40 | end 41 | build_cache! 42 | end 43 | 44 | def lookup(key, scope, order_override, resolution_type) 45 | 46 | answer = nil 47 | 48 | paths = @config[:paths].map { |p| Backend.parse_string(p, scope, { 'key' => key }) } 49 | paths.insert(0, order_override) if order_override 50 | 51 | paths.each do |path| 52 | if path == 'services' 53 | if @cache.has_key?(key) 54 | answer = @cache[key] 55 | return answer 56 | end 57 | end 58 | Hiera.debug("[hiera-consul]: Lookup #{path}/#{key} on #{@config[:host]}:#{@config[:port]}") 59 | # Check that we are not looking somewhere that will make hiera crash subsequent lookups 60 | if "#{path}/#{key}".match("//") 61 | Hiera.debug("[hiera-consul]: The specified path #{path}/#{key} is malformed, skipping") 62 | next 63 | end 64 | # We only support querying the catalog or the kv store 65 | if path !~ /^\/v\d\/(catalog|kv)\// 66 | Hiera.debug("[hiera-consul]: We only support queries to catalog and kv and you asked #{path}, skipping") 67 | next 68 | end 69 | answer = wrapquery("#{path}/#{key}") 70 | next unless answer 71 | break 72 | end 73 | answer 74 | end 75 | 76 | def parse_result(res) 77 | require 'base64' 78 | answer = nil 79 | if res == "null" 80 | Hiera.debug("[hiera-consul]: Jumped as consul null is not valid") 81 | return answer 82 | end 83 | # Consul always returns an array 84 | res_array = JSON.parse(res) 85 | # See if we are a k/v return or a catalog return 86 | if res_array.length > 0 87 | if res_array.first.include? 'Value' 88 | if res_array.first['Value'] == nil 89 | # The Value is nil so we return it directly without trying to decode it ( which would fail ) 90 | return answer 91 | else 92 | answer = Base64.decode64(res_array.first['Value']) 93 | end 94 | else 95 | answer = res_array 96 | end 97 | else 98 | Hiera.debug("[hiera-consul]: Jumped as array empty") 99 | end 100 | return answer 101 | end 102 | 103 | private 104 | 105 | def token(path) 106 | # Token is passed only when querying kv store 107 | if @config[:token] and path =~ /^\/v\d\/kv\// 108 | return "?token=#{@config[:token]}" 109 | else 110 | return nil 111 | end 112 | end 113 | 114 | def wrapquery(path) 115 | 116 | httpreq = Net::HTTP::Get.new("#{path}#{token(path)}") 117 | answer = nil 118 | begin 119 | result = @consul.request(httpreq) 120 | rescue Exception => e 121 | Hiera.debug("[hiera-consul]: Could not connect to Consul") 122 | raise Exception, e.message unless @config[:failure] == 'graceful' 123 | return answer 124 | end 125 | unless result.kind_of?(Net::HTTPSuccess) 126 | Hiera.debug("[hiera-consul]: HTTP response code was #{result.code}") 127 | return answer 128 | end 129 | Hiera.debug("[hiera-consul]: Answer was #{result.body}") 130 | answer = parse_result(result.body) 131 | return answer 132 | end 133 | 134 | def build_cache! 135 | services = wrapquery('/v1/catalog/services') 136 | return nil unless services.is_a? Hash 137 | services.each do |key, value| 138 | service = wrapquery("/v1/catalog/service/#{key}") 139 | next unless service.is_a? Array 140 | service.each do |node_hash| 141 | node = node_hash['Node'] 142 | node_hash.each do |property, value| 143 | # Value of a particular node 144 | next if property == 'ServiceID' 145 | unless property == 'Node' 146 | @cache["#{key}_#{property}_#{node}"] = value 147 | end 148 | unless @cache.has_key?("#{key}_#{property}") 149 | # Value of the first registered node 150 | @cache["#{key}_#{property}"] = value 151 | # Values of all nodes 152 | @cache["#{key}_#{property}_array"] = [value] 153 | else 154 | @cache["#{key}_#{property}_array"].push(value) 155 | end 156 | end 157 | end 158 | end 159 | Hiera.debug("[hiera-consul]: Cache #{@cache}") 160 | end 161 | 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of 20 | fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 21 | ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, 28 | and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to 33 | other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or Object 36 | form, made available under the License, as indicated by a copyright notice 37 | that is included in or attached to the work (an example is provided in the 38 | Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object form, 41 | that is based on (or derived from) the Work and for which the editorial 42 | revisions, annotations, elaborations, or other modifications represent, 43 | as a whole, an original work of authorship. For the purposes of this 44 | License, Derivative Works shall not include works that remain separable 45 | from, or merely link (or bind by name) to the interfaces of, the Work and 46 | Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including the original 49 | version of the Work and any modifications or additions to that Work or 50 | Derivative Works thereof, that is intentionally submitted to Licensor for 51 | inclusion in the Work by the copyright owner or by an individual or 52 | Legal Entity authorized to submit on behalf of the copyright owner. 53 | For the purposes of this definition, "submitted" means any form of 54 | electronic, verbal, or written communication sent to the Licensor or its 55 | representatives, including but not limited to communication on electronic 56 | mailing lists, source code control systems, and issue tracking systems 57 | that are managed by, or on behalf of, the Licensor for the purpose of 58 | discussing and improving the Work, but excluding communication that is 59 | conspicuously marked or otherwise designated in writing by the copyright 60 | owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity on 63 | behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. 67 | 68 | Subject to the terms and conditions of this License, each Contributor 69 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 70 | royalty-free, irrevocable copyright license to reproduce, prepare 71 | Derivative Works of, publicly display, publicly perform, sublicense, 72 | and distribute the Work and such Derivative Works in 73 | Source or Object form. 74 | 75 | 3. Grant of Patent License. 76 | 77 | Subject to the terms and conditions of this License, each Contributor 78 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, 79 | royalty-free, irrevocable (except as stated in this section) patent 80 | license to make, have made, use, offer to sell, sell, import, and 81 | otherwise transfer the Work, where such license applies only to those 82 | patent claims licensable by such Contributor that are necessarily 83 | infringed by their Contribution(s) alone or by combination of their 84 | Contribution(s) with the Work to which such Contribution(s) was submitted. 85 | If You institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 87 | Contribution incorporated within the Work constitutes direct or 88 | contributory patent infringement, then any patent licenses granted to 89 | You under this License for that Work shall terminate as of the date such 90 | litigation is filed. 91 | 92 | 4. Redistribution. 93 | 94 | You may reproduce and distribute copies of the Work or Derivative Works 95 | thereof in any medium, with or without modifications, and in Source or 96 | Object form, provided that You meet the following conditions: 97 | 98 | 1. You must give any other recipients of the Work or Derivative Works a 99 | copy of this License; and 100 | 101 | 2. You must cause any modified files to carry prominent notices stating 102 | that You changed the files; and 103 | 104 | 3. You must retain, in the Source form of any Derivative Works that You 105 | distribute, all copyright, patent, trademark, and attribution notices from 106 | the Source form of the Work, excluding those notices that do not pertain 107 | to any part of the Derivative Works; and 108 | 109 | 4. If the Work includes a "NOTICE" text file as part of its distribution, 110 | then any Derivative Works that You distribute must include a readable copy 111 | of the attribution notices contained within such NOTICE file, excluding 112 | those notices that do not pertain to any part of the Derivative Works, 113 | in at least one of the following places: within a NOTICE text file 114 | distributed as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, within a 116 | display generated by the Derivative Works, if and wherever such 117 | third-party notices normally appear. The contents of the NOTICE file are 118 | for informational purposes only and do not modify the License. 119 | You may add Your own attribution notices within Derivative Works that You 120 | distribute, alongside or as an addendum to the NOTICE text from the Work, 121 | provided that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and may 125 | provide additional or different license terms and conditions for use, 126 | reproduction, or distribution of Your modifications, or for any such 127 | Derivative Works as a whole, provided Your use, reproduction, and 128 | distribution of the Work otherwise complies with the conditions 129 | stated in this License. 130 | 131 | 5. Submission of Contributions. 132 | 133 | Unless You explicitly state otherwise, any Contribution intentionally 134 | submitted for inclusion in the Work by You to the Licensor shall be under 135 | the terms and conditions of this License, without any additional 136 | terms or conditions. Notwithstanding the above, nothing herein shall 137 | supersede or modify the terms of any separate license agreement you may 138 | have executed with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. 141 | 142 | This License does not grant permission to use the trade names, trademarks, 143 | service marks, or product names of the Licensor, except as required for 144 | reasonable and customary use in describing the origin of the Work and 145 | reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. 148 | 149 | Unless required by applicable law or agreed to in writing, Licensor 150 | provides the Work (and each Contributor provides its Contributions) 151 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 152 | either express or implied, including, without limitation, any warranties 153 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS 154 | FOR A PARTICULAR PURPOSE. You are solely responsible for determining the 155 | appropriateness of using or redistributing the Work and assume any risks 156 | associated with Your exercise of permissions under this License. 157 | 158 | 8. Limitation of Liability. 159 | 160 | In no event and under no legal theory, whether in tort 161 | (including negligence), contract, or otherwise, unless required by 162 | applicable law (such as deliberate and grossly negligent acts) or agreed 163 | to in writing, shall any Contributor be liable to You for damages, 164 | including any direct, indirect, special, incidental, or consequential 165 | damages of any character arising as a result of this License or out of 166 | the use or inability to use the Work (including but not limited to damages 167 | for loss of goodwill, work stoppage, computer failure or malfunction, 168 | or any and all other commercial damages or losses), even if such 169 | Contributor has been advised of the possibility of such damages. 170 | 171 | 9. Accepting Warranty or Additional Liability. 172 | 173 | While redistributing the Work or Derivative Works thereof, You may choose 174 | to offer, and charge a fee for, acceptance of support, warranty, 175 | indemnity, or other liability obligations and/or rights consistent with 176 | this License. However, in accepting such obligations, You may act only 177 | on Your own behalf and on Your sole responsibility, not on behalf of any 178 | other Contributor, and only if You agree to indemnify, defend, and hold 179 | each Contributor harmless for any liability incurred by, or claims 180 | asserted against, such Contributor by reason of your accepting any such 181 | warranty or additional liability. 182 | 183 | END OF TERMS AND CONDITIONS 184 | 185 | APPENDIX: How to apply the Apache License to your work 186 | 187 | To apply the Apache License to your work, attach the following boilerplate 188 | notice, with the fields enclosed by brackets "[]" replaced with your own 189 | identifying information. (Don't include the brackets!) The text should be 190 | enclosed in the appropriate comment syntax for the file format. We also 191 | recommend that a file or class name and description of purpose be included 192 | on the same "printed page" as the copyright notice for easier 193 | identification within third-party archives. 194 | 195 | Copyright 2013 Gareth Rushgrove 196 | 197 | Licensed under the Apache License, Version 2.0 (the "License"); 198 | you may not use this file except in compliance with the License. 199 | You may obtain a copy of the License at 200 | 201 | http://www.apache.org/licenses/LICENSE-2.0 202 | 203 | Unless required by applicable law or agreed to in writing, software 204 | distributed under the License is distributed on an "AS IS" BASIS, 205 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 206 | or implied. See the License for the specific language governing 207 | permissions and limitations under the License. 208 | --------------------------------------------------------------------------------