knife-stencil 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Introduction
2
+
3
+ `knife-stencil` is a plugin for the command-line client of [Chef][6]. It enables teams to launch hosts in multiple clouds simply by following their own naming convention. It is brought to you by the folks at [OpsUnit][1].
4
+
5
+ ## Description
6
+
7
+ When provisioning infrastructures with Chef, traditionally one has invoked a long command containing multiple switches. In larger teams, or teams with tiers of engineers this unfortunately leads to errors, as nodes are launched on the incorrect instance size or in incorrect security groups, and also means that there are some aspects of your infrastructure that you cannot recreate from source control.
8
+
9
+ Whilst tools such as [spiceweasel][2] and [ironfan][3] variously provide ways to serialize and orchestrate Chef infrastructures above the level of individual nodes, our current and prior experience with client implementations calls for a way of solving these problems with more flexibility than the former, and in a simpler and less opinionated manner than the latter.
10
+
11
+ `knife-stencil` expresses knife configuration options using a system of templates (stencils) that can inherit and override each other. Stencils are selected based on user-defined regexes that describe various potential node names, as befits your existing site naming convention.
12
+
13
+ The upshot of all of this is that the plugin enables:
14
+
15
+ * Launching hosts with the correct configuration options simply by following your local naming convention
16
+ * The capture in source control of node meta-data such as availability zone, volume persistence and cloud provider (more on this below).
17
+ * Easy operational integration with your existing site, conventions and infrastructure
18
+
19
+ ## Low Friction Operation across multiple cloud providers
20
+
21
+ The plugin also works with most knife-cloud plugins automatically. With the correct stencil configuration it is simple to launch hosts in multiple clouds as selected for by their name. Adding new clouds is simply a matter of configuring the appropriate credentials and installing the corresponding knife plugin gem.
22
+
23
+ ## What does this enable me to do?
24
+
25
+ Some examples of what can be achieved with `knife-stencil` are:
26
+
27
+ * **Launch hosts in different clouds depending on their environment:** launch development nodes on a local Openstack farm, DR servers in HP's cloud and production servers in Amazon EC2.
28
+ * **Ensure hosts land in the correct availability zone depending on some aspect of their role:** ensure all slave database servers are in a different AZ to the masters
29
+ * **Dynamically determine host instance type based on client:** client bigcorp has a premium contract, launch their database servers as an m2.4xlarge, otherwise launch as m1.xlarge
30
+ * **Split teams into "tooling" and "operational" streams:** avoid resorting to runbooks to capture knowledge; reproduce it programatically and versioned over time
31
+ * **Enforce legal juristiction restrictions:** all hosts containing "backup" for client "bigcorp" should be in the EU region
32
+
33
+ # How does it work?
34
+
35
+ Stencils take the general form:
36
+
37
+ {
38
+ "matches": "^[00-99]+\.production\.bigclient\.mycompany\.com$",
39
+ "inherits": [
40
+ "environments/production.json",
41
+ "clients/bigclient.json"
42
+ ],
43
+ "options": {
44
+ ":availability_zone": "us-west-1a"
45
+ }
46
+
47
+ Invoking the plugin to launch a new node: `knife stencil server create -N 1380211968.production.bigclient.mycompany.com` will cause all stencil files under ~/.chef/stencils (configurable via `:stencil_root` in your knife.rb) to be evaluated.
48
+
49
+ Those that express a `matches` regular expression will be compared to the passed node name and ranked in order of strength of match. Once a "root" stencil has been selected any stencils specified through `inherits` will also be evaluated. This continues sequentially and iteratively until a final configuration is arrived upon, at which point the correct cloud server creation class will be instantiated and run.
50
+
51
+ Where options are expressed through inherited stencils exist for which there is already a configuration value, that value will be overwritten. The inhertence tree of a stencil is evaluated in **reverse** order. That is to say, the further away the inherited stencil from the matching "root" stencil the more generic it is intended to be.
52
+
53
+ Not all stencils need express `matches` - nor do they need to inherit anything or express an option. You may choose to create stencils whos job it is to provide a convenient point to aggregate many other stencils, for example.
54
+
55
+ If all this sounds complicated you may find the [examples][4] directory helps to clear things up. You can also invoke `knife stencil server explain live-pretendtown-app00` to have the plugin show you a non-destructive "query plan" of how it arrived at the configuration it would launch the node with. Try this with `:stencil_root` pointing at the 'deadfish' subdirectory of the examples directory.
56
+
57
+ You are free to organise your stencils, their names and the directory structure they are stored under as you wish. The [examples][4] directory provides just that.
58
+
59
+ ## Launching across multiple clouds
60
+
61
+ If the final `:options` hash of a server that is being launched via knife-stencil contains the key `:plugin`, `knife-stencil` will attempt to load and instantiate classes from a similarly-named knife plugin. For example a value of `ec2` will have `knife-stencil` look for the `knife-ec2` plugin and the class `Chef::Knife::Ec2ServerCreate` with which to launch the node. Likewise `hp` with `knife-hp` and `Chef::Knife::HpServerCreate` and so-on for all plugins that follow this standard convention.
62
+
63
+ Given that the cloud plugin to use is expressed via the options hash, which is overridden as inherited stencils are parsed, it is nearly as trivial to provision in other clouds as it is to specify security groups or EBS retention policies or instance size.
64
+
65
+ ## Specifying Configuration Options
66
+ Options are specified using the form they take in the cloud plugin's `options` hash. The easiest way to locate the option you are looking for is:
67
+
68
+ 1. `knife hp server create --help` (note switch)
69
+ 2. Execute `gem environment` to find the root of the GEM_PATH into which your gems are being installed
70
+ 3. Locate the cloud gem and move into lib/chef/knife
71
+ 4. Grep or otherwise locate the option stanza (there may be more than one) for the option you are concerned with
72
+ 5. Take the symbol from the `option' and use that in the configuration:
73
+
74
+ For example, the following options stanza:
75
+
76
+ option :rackspace_servicelevel_wait,
77
+ :long => "--rackspace-servicelevel-wait",
78
+ :description => "Wait until the Rackspace service level automation setup is complete before bootstrapping chef",
79
+ :boolean => true,
80
+ :default => false
81
+
82
+ Yields the following option:
83
+
84
+ "options": {
85
+ "rackspace_servicelevel_wait": true,
86
+ }
87
+
88
+ # Installing and getting started
89
+
90
+ 1. Install the gem
91
+
92
+ `gem install knife-stencil`
93
+
94
+ 2. Configure knife.rb to point `:stencil_root` to your preferred location, or create ~/.chef/stencils
95
+
96
+ `mkdir -p ~/.chef/stencils`
97
+
98
+ OR
99
+
100
+ add `knife[:stencil_root] = "/alternative/location"` to `~/.chef/knife.rb`
101
+
102
+ 3. Use the [examples][4] to start building your stencils and `knife stencil server explain HOSTNAME` to debug and explore. Try copying them into `~/.chef/stencils`.
103
+
104
+ 4. Manage ~/.chef/stencils using your source control workflow
105
+
106
+ ## The Deletion Caveat
107
+ At the time of writing only the knife-ec2 plugin from version 0.6.6 supports deletion via `:chef_node_name`. Most other plugins require an instance-id or similar parameter to be passed to delete a server. This is in counterpoint to how the stencil plugin would like to work. If the other plugins supported [this kind of thing][5] deletion would be a unified interface, like the creation of servers.
108
+
109
+ ## Known Problems
110
+
111
+ * The plugin will not work with chef/knife 11.8.0 or above, due to changes in the internal options parsing code at that point.
112
+
113
+ ## Development
114
+
115
+ [![Build Status](https://travis-ci.org/opsunit/knife-stencil.png?branch=master)](https://travis-ci.org/opsunit/knife-stencil)
116
+
117
+ The gem and its dependencies are tested against the following ruby versions:
118
+
119
+ * 1.9.3
120
+ * 2.0.0
121
+ * 2.1.0
122
+
123
+ [1]: http://www.opsunit.com
124
+ [2]: https://github.com/mattray/spiceweasel/
125
+ [3]: https://github.com/infochimps-labs/ironfan
126
+ [4]: https://github.com/opsunit/knife-stencil/tree/master/examples
127
+ [5]: https://github.com/opscode/knife-ec2/commit/169350ab0dcf11e7e5c224a1c2333707f0364c54
128
+ [6]: http://www.getchef.com/
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => [:say_hello]
3
+
4
+ task :say_hello do
5
+ puts "Hello. I do nothing at the minute."
6
+ end
@@ -0,0 +1,3 @@
1
+ site :opscode
2
+
3
+ cookbook 'hello_world'
@@ -0,0 +1,7 @@
1
+ {
2
+ "sources": {
3
+ "hello_world": {
4
+ "locked_version": "0.0.3"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ # Requirements
2
+ In order to use the examples in this directory you will need to install the following gems:
3
+
4
+ * knife-ec2
5
+ * knife-hp
6
+
7
+ You will also need the [`hello_world`][2] cookbook.
8
+
9
+ If you use [Berkshelf][1] you can install the cookbook requirements from within this directory with:
10
+
11
+ ```bash
12
+ berks install
13
+ berks upload
14
+ ```
15
+
16
+ You will need accounts on at least one of [AWS][3] or [HP Cloud][4] , preferably both.
17
+
18
+ [1]: http://berkshelf.com/
19
+ [2]: http://community.opscode.com/cookbooks/hello_world
20
+ [3]: http://aws.amazon.com
21
+ [4]: http://www.hpcloud.com/
@@ -0,0 +1,14 @@
1
+ {
2
+ "inherits": [
3
+ ],
4
+ "options": {
5
+ "ssh_user": "root",
6
+ "ssh_port": "22",
7
+ "identity_file": "",
8
+ "host_key_verify": false,
9
+ "prerelease": false,
10
+ "environment": "_default",
11
+ "run_list": ["recipe[hello_world]", "recipe[hello_world]"]
12
+ }
13
+ }
14
+
@@ -0,0 +1,23 @@
1
+ {
2
+ "inherits": [
3
+ "base.json"
4
+ ],
5
+ "options": {
6
+ "plugin": "ec2",
7
+ "image": "ami-fbb2fc92",
8
+ "aws_access_key_id": "CHANGEME",
9
+ "aws_secret_access_key": "CHANGEME",
10
+ "aws_ssh_key_id": "CHANGEME",
11
+ "flavor": "m1.small",
12
+ "ssh_user": "ubuntu",
13
+ "identity_file": "CHANGEME",
14
+ "distro": "chef-full",
15
+ "host_key_verify": false,
16
+ "no_host_key_verify": true,
17
+ "security_group_ids": [
18
+ "CHANGEME",
19
+ "CHANGEME"
20
+ ]
21
+ }
22
+ }
23
+
@@ -0,0 +1,19 @@
1
+ {
2
+ "inherits": [
3
+ "base.json"
4
+ ],
5
+ "options": {
6
+ "plugin": "hp",
7
+ "flavor": "101",
8
+ "image": "75845",
9
+ "distro": "chef-full",
10
+ "hp_tenant_id": "CHANGEME",
11
+ "hp_secret_key": "CHANGEME",
12
+ "hp_access_key": "CHANGEME",
13
+ "hp_ssh_key_id": "CHANGEME",
14
+ "host_key_verify": false,
15
+ "no_host_key_verify": true,
16
+ "identity_file": "CHANGEME",
17
+ "ssh_user": "ubuntu"
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "inherits": [
3
+ ],
4
+ "options": {
5
+ }
6
+ }
7
+
@@ -0,0 +1,9 @@
1
+ {
2
+ "matches": "live-pretendtown-app[00-99]+",
3
+ "inherits": [
4
+ "environments/live.json",
5
+ "services/app.json"
6
+ ],
7
+ "options": {
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "matches": "live-pretendtown-lb[00-99]+",
3
+ "inherits": [
4
+ "environments/live.json",
5
+ "services/lb.json"
6
+ ],
7
+ "options": {
8
+ }
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "inherits": [
3
+ "clouds/ec2.json"
4
+ ],
5
+ "options": {
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "inherits": [
3
+ "clouds/hp.json"
4
+ ],
5
+ "options": {
6
+ }
7
+ }
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'knife-stencil/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "knife-stencil"
8
+ spec.version = Knife::Stencil::VERSION
9
+ spec.authors = ["sam"]
10
+ spec.email = ["sam.pointer@opsunit.com"]
11
+ spec.description = %q{Chef knife plugin stencil system with multi-cloud support}
12
+ spec.summary = %q{Chef Knife plugin stencil system. Pass only the hostname and inherit all other options. Seamlessly launch in multiple clouds}
13
+ spec.homepage = "https://github.com/opsunit/knife-stencil"
14
+ spec.license = "GPL3"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake", "~> 0.9"
23
+
24
+ spec.add_dependency "chef", "~> 11.6.0"
25
+ spec.add_dependency "json"
26
+ end
@@ -0,0 +1,86 @@
1
+ #
2
+ # Copyright 2013, OpsUnit Ltd.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'chef/knife'
19
+
20
+ class Chef
21
+ class Knife
22
+
23
+ # This module is included everywhere, as per the conventions for
24
+ # knife plugins. It includes various helper methods.
25
+
26
+ module StencilBase
27
+
28
+ # This is used seemingly at random inside knife. Force our values.
29
+ def locate_config_value(key)
30
+ key = key.to_sym
31
+ config[key] || Chef::Config[:knife][key]
32
+ end
33
+
34
+ # Return true if the stencil plugin has been invoked
35
+ def invoked_as_stencil?
36
+ if $*[0].downcase == 'stencil'
37
+ return true
38
+ end
39
+
40
+ return false
41
+ end
42
+
43
+ # Determine where to look for stencils
44
+ def stencil_root
45
+ stencil_root = '/'
46
+ unless Chef::Config[:knife][:stencil_root] && Dir.exist?(Chef::Config[:knife][:stencil_root]) && stencil_root = Chef::Config[:knife][:stencil_root]
47
+ [ '/etc/chef/stencils', "#{File.join(ENV['HOME'], '.chef/stencils')}" ].each do |directory|
48
+ stencil_root = directory if Dir.exist?(directory)
49
+ end
50
+ end
51
+
52
+ return stencil_root
53
+ end
54
+
55
+ # Do exactly that
56
+ def normalize_path(path, root)
57
+ unless path[0] == File::SEPARATOR # FIXME: This will probably make this nasty on Windows
58
+ return File.join(root, path).to_s
59
+ else
60
+ return path
61
+ end
62
+ end
63
+
64
+ # Return a real Class built from the options given. Used to build EC2ServerCreate, for example
65
+ def build_plugin_klass(plugin, command, action)
66
+ begin
67
+ klass = Object.const_get('Chef').const_get('Knife').const_get(plugin.to_s.capitalize + command.to_s.capitalize + action.to_s.capitalize)
68
+ klass.respond_to?(:new)
69
+ rescue NameError, NoMethodError => e
70
+ puts("I can't find the correct gem for plugin #{config[:plugin]}, it does not declare class #{klas}, or that class does not repsond to 'new'. #{e}")
71
+ puts("Try: gem install knife-#{config[:plugin]}")
72
+ end
73
+
74
+ return klass
75
+ end
76
+
77
+ # Output method for explanation sub-command. Very basic at present.
78
+ def explain(string)
79
+ if $*[2].to_s.downcase == "explain"
80
+ puts "EXPLAIN: #{string}"
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,96 @@
1
+ #
2
+ # Copyright 2013, OpsUnit Ltd.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #
17
+
18
+ require 'chef/knife/stencil_base'
19
+ require 'chef/knife/stencil_file'
20
+
21
+ class Chef
22
+ class Knife
23
+
24
+ # A collection of all StencilFile objects
25
+ class StencilCollection < Array
26
+
27
+ include Knife::StencilBase
28
+
29
+ def initialize(options={})
30
+ Dir.glob(File.join(stencil_root, "**/*.json")).each do |file|
31
+ self << Knife::StencilFile.new(normalize_path(file, stencil_root))
32
+ end
33
+ end
34
+
35
+ # Return stencil for a given name
36
+ def for_name(name)
37
+ collection = Array.new
38
+ collection.push(best_match(name))
39
+
40
+ collection.each do |stencil_file|
41
+
42
+ stencil_file.inherits.each do |path|
43
+ collection << self.stencil_for_path(normalize_path(path, stencil_root))
44
+ end
45
+
46
+ end
47
+
48
+ collection.reverse.each {|t| explain("#{t.path} overrides #{t.inherits} and supplies options #{t.options}")}
49
+ return collection.reverse
50
+ end
51
+
52
+ # Return stencil that best matches a given node name
53
+ def best_match(name)
54
+ weight_stencil_file_map = Hash.new
55
+
56
+ self.each do |stencil_file|
57
+ if stencil_file.matches
58
+ regex = Regexp.new(stencil_file.matches)
59
+ if name.scan(regex).size > 0
60
+ weight = ( name.scan(regex)[0].size || 0 )
61
+ weight_stencil_file_map[weight] = stencil_file
62
+ end
63
+ end
64
+ end
65
+
66
+ unless weight_stencil_file_map.keys.size > 0
67
+ raise ArgumentError, 'No stencil can be found to match that name'
68
+ end
69
+
70
+ match = weight_stencil_file_map.sort_by{|k,v| k}.reverse.first[1] # The StencilFile with the most matched chars
71
+ explain("decision tree: #{weight_stencil_file_map.sort_by{|k,v| k}.reverse}")
72
+
73
+ explain("determined #{match.path} to be the best matched root stencil via #{match.matches}")
74
+ return match
75
+ end
76
+
77
+ # For a given path, determine which stencil matches
78
+ def stencil_for_path(path)
79
+ matched_stencil = nil
80
+
81
+ self.each do |stencil_file|
82
+ if stencil_file.path == path
83
+ matched_stencil = stencil_file
84
+ end
85
+ end
86
+
87
+ if matched_stencil
88
+ return matched_stencil
89
+ else
90
+ raise ArgumentError, "#{path} not found in StencilCollection"
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end