ec2_bootstrap 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 50318b3c980d39aa1255261e3721ebcd5b167731
4
+ data.tar.gz: 4a43edf57231b9a9b5368f933f6925e3e08ce995
5
+ SHA512:
6
+ metadata.gz: 1395ad633953e8d5c09a39e55c0a81e0089df51dae759fb1672373556d80223150d86a21eae4dcb16fb3e06affeddf97eb149cb07d600c60a101bcd9222a9443
7
+ data.tar.gz: ad1a5bacefbeb3a9de43c9885fc6c0e50ab994bde1e37abf6b71a1c85c2aad1cdd7546ed68c9b0f62d16e93a48abe3999f23821c6919c295bc21f974b80b8626
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Cozy Services Ltd.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Ec2Bootstrap
2
+
3
+ A simple wrapper for the EC2 Knife plugin to automate the creation of new EC2 instances.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ec2_bootstrap'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ec2_bootstrap
20
+
21
+ ## Configuration
22
+
23
+ Requires AWS credentials in a format compatible with the [EC2 Knife plugin](https://github.com/chef/knife-ec2/blob/master/README.md).
24
+
25
+ Also requires a YAML config file that looks like the example `config.example.yml`. The config file must include a top-level `nodes` key whose value is an array of hashes. Each node must include the keys `node_name` and `knife_ec2_flags`.
26
+
27
+ You may also want to include some form of cloud-init config. To do this, you can do one of two things:
28
+
29
+ 1. Include a top-level `cloud_config` key with the contents you'd like in the cloud config files used for each node, and ec2_bootstrap will generate separate config files for each node that all include the node's hostname and fqdn.
30
+ 2. If you'd prefer to write your own cloud config files, you can include the cloud config's path in a `user-data` key in the node's `knife_ec2_flags` hash.
31
+
32
+ For any `knife_EC2_flags` values that are lists, they need to be formatted as one long string with values separated by commas. Ex: `security-group-ids: sg-12345678,sg-abcdef12`.
33
+
34
+ ## Usage
35
+
36
+ $ bundle exec ec2_bootstrap -c CONFIG_FILE [options]
37
+
38
+ Passing in a config file with `-c` is required.
39
+
40
+ By default, `ec2_bootstrap` is set to dryrun mode, where `ec2_bootstrap` will
41
+ print out what config would be used to create a new EC2 instance without
42
+ actually creating the instance.
43
+
44
+ $ bundle exec ec2_bootstrap --help
45
+ # help menu
46
+ $ bundle exec ec2_bootstrap -c CONFIG_FILE
47
+ # runs ec2_bootstrap in dryrun mode
48
+ $ bundle exec ec2_bootstrap -c CONFIG_FILE --no-dryrun
49
+ # runs ec2_bootstrap and actually creates a new EC2 instance
50
+ $ bundle exec ec2_bootstrap -c CONFIG_CILE -v
51
+ # runs ec2_bootstrap in verbose mode
52
+
53
+ ## Contributing
54
+
55
+ 1. Fork it ( https://github.com/[my-github-username]/ec2_bootstrap/fork )
56
+ 2. Install dependencies (`bundle install`)
57
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 4. Make your changes.
59
+ 5. Run the tests and make sure they pass (`bundle exec rspec`)
60
+ 6. Commit your changes (`git commit -am 'Add some feature'`)
61
+ 7. Push to the branch (`git push origin my-new-feature`)
62
+ 8. Create a new pull request.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/ec2_bootstrap ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ lib = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'optparse'
6
+ require 'ec2_bootstrap'
7
+
8
+ options = {
9
+ dryrun: true,
10
+ verbose: false
11
+ }
12
+
13
+ OptionParser.new do |opts|
14
+ opts.banner = 'Usage: ec2_bootstrap -c CONFIG_FILE [options]'
15
+
16
+ opts.on('-c', '--config CONFIG_FILE',
17
+ 'Path to custom instance config') do |file|
18
+ options[:config] = file
19
+ end
20
+
21
+ opts.on('-d', '--[no-]dryrun',
22
+ 'Print out changes without actually creating a new EC2 instance (default: true)') do |dryrun|
23
+ options[:dryrun] = dryrun
24
+ end
25
+
26
+ opts.on('-v', '--[no-]verbose',
27
+ 'Run in verbose mode (default: false)') do |verbose|
28
+ options[:verbose] = verbose
29
+ end
30
+ end.parse!
31
+
32
+ config_file = options.delete(:config)
33
+
34
+ unless config_file
35
+ puts "You must specify a config file!"
36
+ exit 1
37
+ end
38
+
39
+ EC2Bootstrap.from_config_file(config_file, options[:dryrun], options[:verbose]).create_instances
@@ -0,0 +1,64 @@
1
+ require 'json'
2
+
3
+ class EC2Bootstrap
4
+ class Instance
5
+
6
+ attr_accessor :name
7
+ attr_accessor :knife_ec2_flags
8
+
9
+ def initialize(instance_name:, knife_ec2_flags:, logger:, domain: nil, json_attributes_file:nil)
10
+ @name = instance_name
11
+ @json_attributes_file = json_attributes_file
12
+ @knife_ec2_flags = build_knife_ec2_flags_hash(knife_ec2_flags)
13
+ @logger = logger
14
+ @domain = domain
15
+ end
16
+
17
+ def build_knife_ec2_flags_hash(knife_ec2_flags)
18
+ knife_ec2_flags['json-attributes'] = "'#{self.load_json_attributes(@json_attributes_file)}'" if @json_attributes_file
19
+
20
+ additional_knife_flags = {
21
+ 'node-name' => @name,
22
+ 'tags' => "Name=#{@name}"
23
+ }
24
+
25
+ return knife_ec2_flags.merge(additional_knife_flags)
26
+ end
27
+
28
+ # Load the JSON and then dump it back out to ensure it's valid JSON.
29
+ # Also makes the JSON easier to read when printing out the command in
30
+ # verbose mode by removing all newlines.
31
+ def load_json_attributes(file_path)
32
+ return JSON.dump(JSON.load(File.read(file_path)))
33
+ end
34
+
35
+ def format_knife_shell_command
36
+ prefix = 'knife ec2 server create '
37
+ knife_flag_array = @knife_ec2_flags.map {|key, value| ['--' + key, value]}.flatten.compact
38
+ return prefix + knife_flag_array.join(' ')
39
+ end
40
+
41
+ def generate_cloud_config(cloud_config, dryrun)
42
+ cloud_config['hostname'] = @name
43
+ cloud_config['fqdn'] = "#{@name}.#{@domain}" if @domain
44
+
45
+ formatted_cloud_config = cloud_config.to_yaml.gsub('---', '#cloud-config')
46
+ cloud_config_path = "cloud_config_#{@name}.txt"
47
+
48
+ if dryrun
49
+ msg = "If this weren't a dry run, I would write the following contents to #{cloud_config_path}:\n#{formatted_cloud_config}"
50
+ @logger.debug(msg)
51
+ else
52
+ self.write_cloud_config_to_file(cloud_config_path, formatted_cloud_config)
53
+ @logger.debug("Wrote cloud config to #{cloud_config_path}.")
54
+ end
55
+
56
+ @knife_ec2_flags['user-data'] = cloud_config_path
57
+ end
58
+
59
+ def write_cloud_config_to_file(path, contents)
60
+ File.open(path, 'w') {|f| f.write(contents)}
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module Ec2Bootstrap
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,75 @@
1
+ require 'yaml'
2
+ require 'open3'
3
+ require 'logger'
4
+
5
+ require 'ec2_bootstrap/version'
6
+ require 'ec2_bootstrap/instance'
7
+
8
+ class EC2Bootstrap
9
+
10
+ attr_accessor :cloud_config
11
+ attr_accessor :instances
12
+ attr_accessor :dryrun
13
+
14
+ def initialize(config, dryrun=true, verbose=false)
15
+ @logger = Logger.new(STDOUT)
16
+ verbose ? @logger.level = Logger::DEBUG : @logger.level = Logger::INFO
17
+
18
+ @cloud_config = config['cloud_config']
19
+ @instances = self.make_instances(config['instances'])
20
+ @dryrun = dryrun
21
+ end
22
+
23
+ def self.from_config(config, *args)
24
+ instances = config['instances']
25
+ raise KeyError, "Config file is missing 'instances' key." unless instances
26
+ raise TypeError, "'instances' config must be an array of hashes." unless instances.is_a?(Array) && instances.first.is_a?(Hash)
27
+ config['instances'] = instances.map {|i| i.map {|key, value| [key.to_sym, value]}.to_h}
28
+
29
+ return self.new(config, *args)
30
+ end
31
+
32
+ def self.from_config_file(config_path, *args)
33
+ config = YAML.load(File.read(config_path))
34
+
35
+ self.from_config(config, *args)
36
+ end
37
+
38
+ def make_instances(instances_config)
39
+ return instances_config.map {|i| self.instance_class.new(i.merge(logger: @logger))}
40
+ end
41
+
42
+ def instance_class
43
+ return Instance
44
+ end
45
+
46
+ def create_instances
47
+ @logger.debug("This was a dry run. No EC2 instances were created.") if @dryrun
48
+
49
+ @instances.each do |instance|
50
+ @logger.debug("Instance name: #{instance.name}")
51
+
52
+ instance.generate_cloud_config(@cloud_config, @dryrun) if @cloud_config
53
+
54
+ knife_shell_command = instance.format_knife_shell_command
55
+ @logger.debug("Knife shell command:\n#{knife_shell_command}")
56
+
57
+ unless @dryrun
58
+ status = self.shell_out_command(knife_shell_command)
59
+ return status
60
+ end
61
+ end
62
+ end
63
+
64
+ def shell_out_command(command)
65
+ STDOUT.sync = true
66
+ Open3::popen2e(command) do |stdin, stdout_and_stderr, wait_thr|
67
+ while (line = stdout_and_stderr.gets) do
68
+ @logger.info(line.strip)
69
+ end
70
+ status = wait_thr.value
71
+ @logger.info("status: #{status}")
72
+ return status
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../spec_helper'
2
+
3
+ describe 'EC2Bootstrap::Instance' do
4
+
5
+ let(:knife_flags_hash) do
6
+ {
7
+ 'availability-zone' => 'us-east-1a',
8
+ 'environment' => 'production',
9
+ 'flavor' => 'm4.large'
10
+ }
11
+ end
12
+
13
+ let(:logger) do
14
+ logger = Logger.new(STDOUT)
15
+ logger.level = Logger::WARN
16
+ logger
17
+ end
18
+
19
+ let(:instance) do
20
+ EC2Bootstrap::InstanceMock.new(
21
+ instance_name: 'pumpkin',
22
+ domain: 'chocolate.muffins.com',
23
+ knife_ec2_flags: knife_flags_hash,
24
+ logger: logger
25
+ )
26
+ end
27
+
28
+ it 'properly formats the knife shell command' do
29
+ knife_command = 'knife ec2 server create --availability-zone us-east-1a --environment production --flavor m4.large'
30
+
31
+ expect(instance.format_knife_shell_command).to include(knife_command)
32
+ end
33
+
34
+ it 'can generate its own cloud config' do
35
+ cloud_config = {
36
+ 'manage_etc_hosts': 'true',
37
+ 'bootcmd': ['do stuff', 'do some more stuff']
38
+ }
39
+
40
+ expect(instance.generate_cloud_config(cloud_config, false)).to eq("cloud_config_#{instance.name}.txt")
41
+ end
42
+
43
+ end
@@ -0,0 +1,95 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe 'EC2Bootstrap' do
4
+
5
+ let(:yaml_content) do
6
+ {
7
+ 'cloud_config' => {
8
+ 'manage_etc_hosts' => 'true',
9
+ 'bootcmd' => ['do stuff', 'do some more stuff']
10
+ },
11
+ 'instances' => [
12
+ {
13
+ 'instance_name' => 'cat',
14
+ 'knife_ec2_flags' => {},
15
+ 'domain' => 'cats.com'
16
+ },
17
+ {
18
+ 'instance_name' => 'mouse',
19
+ 'json_attributes_file' => {},
20
+ 'knife_ec2_flags' => {}
21
+ },
22
+ {
23
+ 'instance_name' => 'whale',
24
+ 'knife_ec2_flags' => {}
25
+ }
26
+ ]
27
+ }
28
+ end
29
+
30
+ context 'loading from the yaml config' do
31
+
32
+ it "raises a KeyError if the yaml content lacks an 'instances' key" do
33
+ yaml = yaml_content.reject {|k,v| k == 'instances'}
34
+
35
+ expect {EC2BootstrapMock.from_config(yaml, false)}.to raise_error(KeyError)
36
+ end
37
+
38
+ it "raises a TypeError if the 'instances' value is not an array of hashes" do
39
+ yaml = yaml_content.merge({'instances' => ['a thing', 'another thing']})
40
+
41
+ expect {EC2BootstrapMock.from_config(yaml, false)}.to raise_error(TypeError)
42
+ end
43
+
44
+ it 'loads successfully if the yaml content is properly formatted' do
45
+ bootstrap = EC2BootstrapMock.from_config(yaml_content, false)
46
+
47
+ expect(bootstrap.cloud_config).to eq(yaml_content['cloud_config'])
48
+ expect(bootstrap.instances).to be_an(Array)
49
+ expect(bootstrap.instances.first).to be_a(EC2Bootstrap::InstanceMock)
50
+ expect(bootstrap.dryrun).to be_falsey
51
+ end
52
+
53
+ end
54
+
55
+ context 'creating instances' do
56
+ context 'generating cloud config' do
57
+
58
+ it "doesn't generate cloud config if it wasn't included at the top level of the yaml config" do
59
+ yaml = yaml_content.reject {|k,v| k == 'cloud_config'}
60
+ bootstrap = EC2BootstrapMock.from_config(yaml, false)
61
+ instance = bootstrap.instances.first
62
+
63
+ expect(instance).to_not receive(:generate_cloud_config)
64
+ bootstrap.create_instances
65
+ end
66
+
67
+ it 'generates cloud config if it was included at the top level in the yaml config' do
68
+ bootstrap = EC2BootstrapMock.from_config(yaml_content, false)
69
+ instance = bootstrap.instances.first
70
+
71
+ expect(instance).to receive(:generate_cloud_config)
72
+ bootstrap.create_instances
73
+ end
74
+
75
+ end
76
+
77
+ context 'shelling out knife EC2 command' do
78
+
79
+ it "doesn't shell out if it's a dryrun" do
80
+ bootstrap = EC2BootstrapMock.from_config(yaml_content, true)
81
+
82
+ expect(bootstrap).to_not receive(:shell_out_command)
83
+ bootstrap.create_instances
84
+ end
85
+
86
+ it "shells out if it's not a dryrun" do
87
+ bootstrap = EC2BootstrapMock.from_config(yaml_content, false)
88
+
89
+ expect(bootstrap).to receive(:shell_out_command)
90
+ bootstrap.create_instances
91
+ end
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,33 @@
1
+ require 'ec2_bootstrap'
2
+ require 'rspec'
3
+
4
+ class EC2BootstrapMock < EC2Bootstrap
5
+
6
+ def instance_class
7
+ return InstanceMock
8
+ end
9
+
10
+ def shell_out_command(command)
11
+ return 0
12
+ end
13
+
14
+ end
15
+
16
+ class EC2Bootstrap
17
+ class InstanceMock < Instance
18
+
19
+ def write_cloud_config_to_file(path, content)
20
+ return content.bytesize
21
+ end
22
+
23
+ def load_json_attributes(json)
24
+ return json
25
+ end
26
+
27
+ end
28
+ end
29
+
30
+ RSpec.configure do |config|
31
+ config.run_all_when_everything_filtered = true
32
+ config.filter_run :focus
33
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2_bootstrap
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cozy Services Ltd.
8
+ - Rachel King
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-03-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.6'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.6'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '10.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.4'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.4'
56
+ - !ruby/object:Gem::Dependency
57
+ name: chef
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '12.5'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '12.5'
70
+ - !ruby/object:Gem::Dependency
71
+ name: knife-ec2
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.12'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.12'
84
+ description: Bootstrap EC2 instances with custom config.
85
+ email:
86
+ - opensource@cozy.co
87
+ executables:
88
+ - ec2_bootstrap
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - bin/ec2_bootstrap
96
+ - lib/ec2_bootstrap.rb
97
+ - lib/ec2_bootstrap/instance.rb
98
+ - lib/ec2_bootstrap/version.rb
99
+ - spec/ec2_bootstrap/instance_spec.rb
100
+ - spec/ec2_bootstrap_spec.rb
101
+ - spec/spec_helper.rb
102
+ homepage: https://github.com/CozyCo/ec2_bootstrap
103
+ licenses:
104
+ - MIT
105
+ metadata: {}
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 2.4.5
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Bootstrap EC2 instances with custom config.
126
+ test_files:
127
+ - spec/ec2_bootstrap/instance_spec.rb
128
+ - spec/ec2_bootstrap_spec.rb
129
+ - spec/spec_helper.rb