knife-stencil 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +128 -0
- data/Rakefile +6 -0
- data/examples/deadfish/Berksfile +3 -0
- data/examples/deadfish/Berksfile.lock +7 -0
- data/examples/deadfish/README.md +21 -0
- data/examples/deadfish/base.json +14 -0
- data/examples/deadfish/clouds/ec2.json +23 -0
- data/examples/deadfish/clouds/hp.json +19 -0
- data/examples/deadfish/environments/live.json +7 -0
- data/examples/deadfish/live-pretendtown-app.json +9 -0
- data/examples/deadfish/live-pretendtown-lb.json +9 -0
- data/examples/deadfish/services/app.json +7 -0
- data/examples/deadfish/services/lb.json +7 -0
- data/knife-stencil.gemspec +26 -0
- data/lib/chef/knife/stencil_base.rb +86 -0
- data/lib/chef/knife/stencil_collection.rb +96 -0
- data/lib/chef/knife/stencil_file.rb +44 -0
- data/lib/chef/knife/stencil_monkey_patch.rb +61 -0
- data/lib/chef/knife/stencil_node.rb +52 -0
- data/lib/chef/knife/stencil_server_create.rb +49 -0
- data/lib/chef/knife/stencil_server_delete.rb +52 -0
- data/lib/chef/knife/stencil_server_explain.rb +44 -0
- data/lib/knife-stencil/version.rb +6 -0
- metadata +128 -0
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,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,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
|