ec2 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 67a8f18079f6dcaab371130b7bb5719adde84853
4
+ data.tar.gz: 3507f5c9ef4d942386a463aac6561e6d6e9ae98e
5
+ SHA512:
6
+ metadata.gz: 6aba022af5c0fd56b1cdcd944715bf30a60dfe376b4574e89494ff9bfb01d3365b77e36608d5701dd60cc17629c4f131f8b82436d637c57cd88dcff7f31254ff
7
+ data.tar.gz: 168ded50b3abd14b310a53c3f8afccaa02a5ca195a36e9cc2e61212a1e77be3d0ed9831de08a82f5b85fe7ef454179e562dcddf1a4c5c2689344928f393ba932
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ec2.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Neeraj Bhunwal
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,31 @@
1
+ # Ec2
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ec2'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ec2
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it ( https://github.com/[my-github-username]/ec2/fork )
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/ec2 ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ Signal.trap("INT") { exit 1 }
3
+
4
+ require 'optparse'
5
+ require 'ec2/version'
6
+ require 'ec2/error'
7
+ require 'ec2/logger'
8
+
9
+ logger = ::Ec2::Logger.logger
10
+ options = {}
11
+
12
+ opts_parser = OptionParser.new do |opts|
13
+
14
+ banner = []
15
+ banner << "Usage: ec2 [global options] command [options] args"
16
+ banner << "Commands:"
17
+ banner << " hosts"
18
+
19
+ banner << "Options: "
20
+ opts.banner = banner.join("\n")
21
+
22
+ opts.on("-v", "--version", "Show version") do |v|
23
+ puts ::Ec2::VERSION
24
+ exit
25
+ end
26
+
27
+ opts.on("--debug", "Show debug messages") do
28
+ options[:debug] = true
29
+ logger.level = ::Logger::DEBUG
30
+ end
31
+
32
+ opts.on("--trace", "Show debug messages and exception stack trace") do
33
+ options[:debug] = true
34
+ options[:trace] = true
35
+ logger.level = ::Logger::DEBUG
36
+ logger.trace = true
37
+ end
38
+
39
+ opts.on_tail("-h", "--help", "Show this message") do
40
+ puts opts
41
+ exit
42
+ end
43
+ end
44
+ begin
45
+ opts_parser.order!(ARGV)
46
+ command = ( ARGV.shift || '').to_sym
47
+ case command
48
+ when :hosts
49
+ require 'ec2/cli/hosts'
50
+ cli = ::Ec2::Cli::Hosts.new(ARGV)
51
+ cli.run
52
+ when :resources
53
+ require 'ec2/cli/resources'
54
+ cli = ::Ec2::Cli::Resources.new(ARGV)
55
+ cli.run
56
+ when :''
57
+ puts opts_parser
58
+ else
59
+ raise ::Ec2::Error, "no such command #{command}"
60
+ end
61
+
62
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument, Ec2::Error => e
63
+ cause = e.cause
64
+ if options[:trace]
65
+ puts cause
66
+ cause ? (raise cause) : (raise e)
67
+ else
68
+ logger.debug "#{cause.message}" if cause
69
+ logger.error "#{e.message}"
70
+ abort
71
+ end
72
+ end
data/dev_bin/ec2 ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
4
+ load "#{__dir__}/../bin/ec2"
data/ec2.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ec2/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ec2"
8
+ spec.version = Ec2::VERSION
9
+ spec.authors = ["Neeraj Bhunwal"]
10
+ spec.email = ["neeraj.bhunwal@gmail.com"]
11
+ spec.summary = %q{Integrates ec2 resources with salt}
12
+ spec.description = %q{Integrates ec2 resources with salt}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
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_runtime_dependency 'aws-sdk', '2.0.23'
22
+ spec.add_development_dependency "bundler", "~> 1.7"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ end
@@ -0,0 +1,10 @@
1
+ import "templates.rb"
2
+
3
+ base_subnet "dev"
4
+ availability_zones "lol5", "lol3"
5
+
6
+ use :app, :postgres
7
+
8
+ mprofile :postgres_small, template: "postgres" do
9
+ volume device: "/dev/xvdf", size: 100
10
+ end
@@ -0,0 +1,10 @@
1
+ base_profile "centos"
2
+
3
+ template "app" do
4
+ security_groups "ssh", "default"
5
+ end
6
+
7
+ template "postgres" do
8
+ security_groups "ssh", "default"
9
+ volume device: '/dev/xvdf', size: 150
10
+ end
data/examples/test.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'pry'
2
+ lib = File.expand_path(__dir__ + '/../lib')
3
+ $LOAD_PATH.unshift(lib)
4
+
5
+ require 'aws-sdk'
6
+ require 'ec2/config'
7
+ require 'ec2/query_api'
8
+ require 'ec2/profile_dsl'
9
+
10
+ config = Ec2::Config.new(".ec2.rb").config
11
+ credentials = Aws::Credentials.new(config[:aws_key], config[:aws_secret])
12
+ Aws.config[:credentials] = credentials
13
+ Aws.config[:region] = config[:region]
14
+
15
+ api = Ec2::QueryApi.new(vpc_id: config[:vpc_id])
16
+
17
+ x = Ec2::ProfileDsl.new("profiles.rb", api: api)
18
+ pry
@@ -0,0 +1,51 @@
1
+ require 'ec2/lock'
2
+ require 'ec2/salt_cloud'
3
+ require 'ec2/config'
4
+
5
+ module Ec2
6
+ module Cli
7
+ class Hosts
8
+
9
+ def initialize(argv)
10
+ @argv = argv
11
+ end
12
+
13
+ def run
14
+ lock.acquire
15
+ opts.parse!(@argv)
16
+ salt.config = config
17
+ salt.run
18
+ puts "ran hosts"
19
+ end
20
+
21
+ def salt
22
+ @salt ||= ::Ec2::SaltCloud.new
23
+ end
24
+
25
+ def lock
26
+ @lock ||= ::Ec2::Lock.new
27
+ end
28
+
29
+ def config
30
+ @config ||= ::Ec2::Config.new("ec2.conf").config
31
+ end
32
+
33
+ def opts
34
+ OptionParser.new do |opts|
35
+
36
+ opts.banner = "Usage: ec2 hosts [options] [target]"
37
+
38
+ opts.on("-f", "--file FILE", "Specify hosts file") do |f|
39
+ salt.hosts_file = f
40
+ end
41
+
42
+ opts.on("-h", "--help", "Help") do
43
+ puts opts
44
+ exit
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,56 @@
1
+ require 'ec2/lock'
2
+ require 'ec2/resources'
3
+ require 'ec2/config'
4
+
5
+ module Ec2
6
+ module Cli
7
+ class Resources
8
+
9
+ def initialize(argv)
10
+ @argv = argv
11
+ end
12
+
13
+ def run
14
+ lock.acquire
15
+ opts.parse!(@argv)
16
+ init_aws
17
+ @argv.each do |file|
18
+ resources = ::Ec2::Resources.new(file)
19
+ resources.apply
20
+ end
21
+ end
22
+
23
+ def lock
24
+ @lock ||= ::Ec2::Lock.new
25
+ end
26
+
27
+
28
+ def init_aws
29
+ credentials = Aws::Credentials.new(config[:aws_key], config[:aws_secret])
30
+ Aws.config[:credentials] = credentials
31
+ Aws.config[:region] = config[:region]
32
+ end
33
+
34
+ def config
35
+ @config ||= ::Ec2::Config.new("ec2.conf").config
36
+ end
37
+
38
+ def opts
39
+ OptionParser.new do |opts|
40
+
41
+ opts.banner = "Usage: ec2 resources [options] [target]"
42
+
43
+ opts.on("-f", "--file FILE", "Specify hosts file") do |f|
44
+ salt.hosts_file = f
45
+ end
46
+
47
+ opts.on("-h", "--help", "Help") do
48
+ puts opts
49
+ exit
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
data/lib/ec2/config.rb ADDED
@@ -0,0 +1,58 @@
1
+ require 'ec2/helper'
2
+ module Ec2
3
+ class Config
4
+
5
+ include Helper
6
+
7
+ def initialize(config_path)
8
+ @config_path = config_path
9
+ end
10
+
11
+ def config
12
+ return @config if @config
13
+ @config = {
14
+ region: "ap-southeast-1",
15
+ }
16
+ read_config
17
+ return @config
18
+ end
19
+
20
+ private
21
+
22
+ def region(region)
23
+ set :region, region
24
+ end
25
+
26
+ def vpc_id(vpc)
27
+ set :vpc_id, vpc
28
+ end
29
+
30
+ def aws_key(aws_key)
31
+ set :aws_key, aws_key
32
+ end
33
+
34
+ def aws_secret(aws_secret)
35
+ set :aws_secret, aws_secret
36
+ end
37
+
38
+ def set(key, value)
39
+ @config.store key, value
40
+ end
41
+
42
+ def read_config
43
+ read("#{Dir.home}/.ec2.rb", required: false)
44
+ read(@config_path, required: false)
45
+ end
46
+
47
+ def read(file, required: true)
48
+ if not File.readable? file
49
+ raise error "config: #{file} not readable" if required
50
+ return
51
+ end
52
+ instance_eval(File.read(file), file)
53
+ rescue NoMethodError => e
54
+ error "invalid option used in config: #{e.name}"
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ require 'ec2/ext/string'
2
+ module Ec2
3
+ class CustomLogger < ::Logger
4
+
5
+ attr_writer :trace
6
+
7
+ def initialize(file)
8
+ super(file)
9
+ @level = ::Logger::INFO
10
+ end
11
+
12
+ def format_message(severity, timestamp, progname, msg)
13
+ case severity
14
+ when "INFO"
15
+ "#{msg}\n"
16
+ when "ERROR"
17
+ "#{severity.bold.red} #{msg}\n"
18
+ when "WARN"
19
+ "#{severity.downcase.bold.yellow} #{msg}\n"
20
+ else
21
+ "#{severity[0].bold.blue} #{msg}\n"
22
+ end
23
+ end
24
+
25
+ def bullet(msg)
26
+ puts "#{"\u2219".bold.blue} #{msg}"
27
+ end
28
+
29
+ def trace?
30
+ @trace
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ require 'ec2/logger'
2
+ require 'ec2/helper'
3
+ require 'erb'
4
+
5
+ module Ec2
6
+ class ErbProfile
7
+
8
+ include Logger
9
+ include Helper
10
+
11
+ attr_accessor :api
12
+
13
+ def initialize(file, api: nil)
14
+ @file = file
15
+ @api = api
16
+ end
17
+
18
+ def binding
19
+ api.instance_eval { binding }
20
+ end
21
+
22
+ def render
23
+ erb = ERB.new(File.read(@file), nil, '-')
24
+ erb.result(binding)
25
+ rescue => e
26
+ error "while rendering erb file #{@file}"
27
+ end
28
+
29
+ end
30
+ end
data/lib/ec2/error.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Ec2
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,33 @@
1
+ class String
2
+ def colorize(color_code)
3
+ "\e[#{color_code}m#{self}\e[0m"
4
+ end
5
+
6
+ def bold
7
+ "\e[1m#{self}\e[0m"
8
+ end
9
+
10
+ def white
11
+ colorize(37)
12
+ end
13
+
14
+ def green
15
+ colorize(32)
16
+ end
17
+
18
+ def yellow
19
+ colorize(33)
20
+ end
21
+
22
+ def red
23
+ colorize(31)
24
+ end
25
+
26
+ def blue
27
+ colorize(34)
28
+ end
29
+
30
+ def grey
31
+ colorize("90")
32
+ end
33
+ end
data/lib/ec2/helper.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'ec2/error'
2
+
3
+ module Ec2
4
+ module Helper
5
+ def error(msg)
6
+ raise ::Ec2::Error, msg
7
+ end
8
+ end
9
+ end
data/lib/ec2/lock.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'ec2/logger'
2
+ module Ec2
3
+ class Lock
4
+
5
+ include Logger
6
+
7
+ def acquire
8
+ logger.debug "acquiring lock"
9
+ lock_acquired = lock_file.flock(File::LOCK_NB | File::LOCK_EX)
10
+ raise "exclusive lock not available" if not lock_acquired
11
+ end
12
+
13
+ def lock_file
14
+ @lock_file ||= File.open(".ec2.lock", "a+")
15
+ end
16
+
17
+ end
18
+ end
19
+
data/lib/ec2/logger.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+ require 'ec2/custom_logger'
3
+ module Ec2
4
+ module Logger
5
+
6
+ def self.logger
7
+ @logger ||= CustomLogger.new(STDOUT)
8
+ end
9
+
10
+ def self.stderr
11
+ @stderr ||= ::Logger.new(STDERR)
12
+ end
13
+
14
+ def logger
15
+ ::Ec2::Logger.logger
16
+ end
17
+
18
+ def stderr
19
+ ::Ec2::Logger.stderr
20
+ end
21
+
22
+ def debug?
23
+ logger.level == ::Logger::DEBUG
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,92 @@
1
+ require 'yaml'
2
+ require 'ec2/logger'
3
+ require 'ec2/helper'
4
+ require 'json'
5
+
6
+ module Ec2
7
+ class Profile
8
+
9
+ include Logger
10
+ include Helper
11
+
12
+ attr_reader :api, :data
13
+
14
+ def initialize(api: nil, data: nil)
15
+ @data = deep_copy(data) || {}
16
+ @api = api
17
+ init_network
18
+ end
19
+
20
+ def create(transform: true, &block)
21
+ instance_eval &block
22
+ transform_name_to_id if transform
23
+ end
24
+
25
+ def init_network
26
+ return if @data['network_interfaces'].is_a? Array
27
+ @data['network_interfaces'] = [
28
+ {
29
+ "DeviceIndex" => 0,
30
+ "AssociatePublicIpAddress" => true,
31
+ "SecurityGroupId" => []
32
+ }
33
+ ]
34
+ end
35
+
36
+ def extends(value)
37
+ @data.store "extends", value
38
+ end
39
+
40
+ def size(value)
41
+ @data.store "size", value
42
+ end
43
+
44
+ def security_group(name, interface: 0)
45
+ @data["network_interfaces"][interface]["SecurityGroupId"] << name
46
+ end
47
+
48
+ def subnet(name, interface: 0)
49
+ @data["network_interfaces"][interface]["SubnetId"] = name
50
+ end
51
+
52
+ def security_groups(*names, interface: 0)
53
+ names.each { |name| security_group name, interface: interface }
54
+ end
55
+
56
+ def volume(device:, type: "gp2", size:)
57
+ @data["volumes"] ||= []
58
+ volumes = @data["volumes"]
59
+ volume = {
60
+ "device" => device,
61
+ "type" => type,
62
+ "size" => size
63
+ }
64
+ existing_volume = volumes.find { |v| v["device"] == device }
65
+ if existing_volume
66
+ existing_volume.merge! volume
67
+ else
68
+ volumes << volume
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def deep_copy(hash)
75
+ return nil if not hash.is_a? Hash
76
+ Marshal.load(Marshal.dump hash)
77
+ end
78
+
79
+
80
+ def transform_name_to_id
81
+ @data["network_interfaces"].each do |n|
82
+ n["SubnetId"] = api.subnet(n["SubnetId"])
83
+ security_groups = n["SecurityGroupId"]
84
+
85
+ security_groups.each_with_index do |name, i|
86
+ logger.debug "resolving security_group #{name}"
87
+ security_groups[i] = api.security_group(name)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,96 @@
1
+ require 'yaml'
2
+ require 'ec2/logger'
3
+ require 'ec2/helper'
4
+ require 'ec2/profile'
5
+ require 'set'
6
+
7
+ module Ec2
8
+ class ProfileDsl
9
+
10
+ include Logger
11
+ include Helper
12
+
13
+ attr_reader :api
14
+
15
+ def initialize(file, api:)
16
+ @file = file
17
+ @profiles = {}
18
+ @api = api
19
+ @templates = {}
20
+ @availability_zones = ["a", "b"]
21
+ @required = Set.new
22
+ end
23
+
24
+ def use(*templates)
25
+ templates.each do |t|
26
+ t = t.to_s
27
+ mprofile(t, template: t){}
28
+ end
29
+ end
30
+
31
+ def template(name, &block)
32
+ profile = Profile.new(api: api)
33
+ profile.extends(@base_profile) if @base_profile
34
+ profile.create(transform: false, &block)
35
+ @templates[name] = profile.data
36
+ @templates[name].freeze
37
+ end
38
+
39
+ def mprofile(name, template: nil, &block)
40
+ data = @templates.fetch template if template
41
+ @availability_zones.each do |az|
42
+ profile = Profile.new(data: data, api: api)
43
+ profile.extends(@base_profile) if @base_profile
44
+ profile.subnet("#{@base_subnet}-#{az}")
45
+ profile.create &block
46
+ profile_name = "#{name}-#{az}"
47
+ profile.data.freeze
48
+ @profiles[profile_name] = profile.data
49
+ end
50
+ end
51
+
52
+ def profile(name, template: nil, &block)
53
+ data = @templates.fetch template if template
54
+ profile = Profile.new(data: data, api: api)
55
+ profile.extends(@base_profile) if @base_profile
56
+ profile.create &block
57
+ profile.data.freeze
58
+ @profiles[name] = profile.data
59
+ end
60
+
61
+ def base_profile(name)
62
+ @base_profile = name
63
+ end
64
+
65
+ def base_subnet(name)
66
+ @base_subnet = name
67
+ end
68
+
69
+ def availability_zones(*zones)
70
+ @availability_zones = zones
71
+ end
72
+
73
+ def render
74
+ if not File.readable? @file
75
+ logger.info "#{@file} not readable"
76
+ return
77
+ end
78
+ instance_eval(File.read(@file), @file)
79
+ YAML.dump @profiles
80
+ rescue NoMethodError => e
81
+ error "invalid option used in profiles config: #{e.name}"
82
+ end
83
+
84
+ def vpc_id(vpc_id)
85
+ @vpc_id = vpc_id
86
+ end
87
+
88
+ def import(path)
89
+ abs_path = File.realpath path
90
+ return if @required.include? abs_path
91
+ instance_eval((File.read abs_path), abs_path)
92
+ @required << abs_path
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,35 @@
1
+ require 'ec2/helper'
2
+ require 'ec2/security_group'
3
+ require 'ec2/subnet'
4
+
5
+ module Ec2
6
+ class QueryApi
7
+
8
+ include Helper
9
+
10
+ def initialize(vpc_id: nil)
11
+ @vpc_id = vpc_id
12
+ end
13
+
14
+ def security_group(name)
15
+ security_group = SecurityGroup.new(name, vpc_id: @vpc_id)
16
+ sg_cache[name] ||= security_group.id!
17
+ end
18
+
19
+ def subnet(name)
20
+ subnet = Subnet.new(name, vpc_id: @vpc_id)
21
+ subnet_cache[name] ||= subnet.id!
22
+ end
23
+
24
+ private
25
+
26
+ def sg_cache
27
+ @sg_cache ||= {}
28
+ end
29
+
30
+ def subnet_cache
31
+ @subnet_cache ||= {}
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ require 'ec2/security_group'
2
+ require 'ec2/subnet'
3
+ module Ec2
4
+ class Resources
5
+
6
+
7
+ def initialize(file)
8
+ @file = file
9
+ end
10
+
11
+ def vpc_id(vpc_id)
12
+ @vpc_id = vpc_id
13
+ end
14
+
15
+ def security_group(name, &block)
16
+ sg = SecurityGroup.new(name, vpc_id: @vpc_id)
17
+ sg.created &block
18
+ end
19
+
20
+ def subnet(name, &block)
21
+ subnet = Subnet.new(name, vpc_id: @vpc_id)
22
+ subnet.created &block
23
+ end
24
+
25
+ def apply
26
+ instance_eval(File.read(@file), @file)
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,112 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+ require 'aws-sdk'
4
+
5
+ require 'ec2/query_api'
6
+ require 'ec2/profile_dsl'
7
+ require 'ec2/erb_profile'
8
+ require 'ec2/logger'
9
+
10
+ module Ec2
11
+ class SaltCloud
12
+
13
+ include Logger
14
+ attr_writer :hosts_file
15
+ attr_accessor :config
16
+
17
+ def initialize
18
+ end
19
+
20
+ def run
21
+ init_working_dir
22
+ copy_providers
23
+ copy_profiles
24
+ copy_master_config
25
+ copy_deploy_scripts
26
+ init_aws
27
+ render_global_profiles
28
+ render_local_profile
29
+ run_salt_cloud
30
+ end
31
+
32
+ private
33
+
34
+ def init_aws
35
+ credentials = Aws::Credentials.new(config[:aws_key], config[:aws_secret])
36
+ Aws.config[:credentials] = credentials
37
+ Aws.config[:region] = config[:region]
38
+ end
39
+
40
+ def api
41
+ @api ||= QueryApi.new(vpc_id: config[:vpc_id])
42
+ end
43
+
44
+ def working_dir
45
+ @working_dir ||= ".salt.tmp"
46
+ end
47
+
48
+ def hosts_file
49
+ @hosts_file ||= "hosts"
50
+ end
51
+
52
+ def init_working_dir
53
+ FileUtils.rm_r working_dir if File.directory? working_dir
54
+ FileUtils.mkdir_p "#{working_dir}/cloud.profiles.d"
55
+ end
56
+
57
+ def copy_providers
58
+ return if not File.directory? "/etc/salt/cloud.providers.d"
59
+ FileUtils.cp_r "/etc/salt/cloud.providers.d", working_dir
60
+ end
61
+
62
+ def copy_profiles
63
+ return if not File.directory? "/etc/salt/cloud.profiles.d"
64
+ Dir["/etc/salt/cloud.profiles.d/*.conf"].each do |f|
65
+ path = Pathname.new f
66
+ FileUtils.cp path, "#{working_dir}/cloud.profiles.d/_#{path.basename}"
67
+ end
68
+ end
69
+
70
+ def copy_master_config
71
+ FileUtils.cp "/etc/salt/master", working_dir
72
+ end
73
+
74
+ def copy_deploy_scripts
75
+ return if not File.directory? "/etc/salt/cloud.deploy.d"
76
+ FileUtils.cp_r "/etc/salt/cloud.deploy.d", working_dir
77
+ end
78
+
79
+ def render_global_profiles
80
+ Dir["/etc/salt/cloud.profiles.d/*.erb"].each do |f|
81
+ render(f, prefix: "_")
82
+ end
83
+ end
84
+
85
+ def render_local_profile
86
+ local_profile = ["profiles.rb", "profiles.erb"].find { |f| File.readable? f }
87
+ render(local_profile) if local_profile
88
+ end
89
+
90
+ def render(path, prefix: nil)
91
+ extname = File.extname path
92
+ name = File.basename(path, extname)
93
+ outfile = "#{working_dir}/cloud.profiles.d/#{prefix}#{name}.conf"
94
+ case extname
95
+ when ".rb"
96
+ profile_dsl = ProfileDsl.new(path, api: api)
97
+ File.open(outfile, "w") { |f| f.write profile_dsl.render }
98
+ when ".erb"
99
+ erb = ErbProfile.new(path, api: api)
100
+ File.open(outfile, "w") { |f| f.write erb.render }
101
+ end
102
+ end
103
+
104
+ def run_salt_cloud
105
+ logger.debug "running salt cloud"
106
+ command = %Q( salt-cloud -m #{hosts_file} -c #{working_dir} )
107
+ command << " -l debug" if logger.trace?
108
+ system command
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,196 @@
1
+ require 'ec2/helper'
2
+ require 'ec2/logger'
3
+ require 'aws-sdk'
4
+ require 'set'
5
+
6
+ module Ec2
7
+ class SecurityGroup
8
+
9
+ include Helper
10
+ include Logger
11
+ attr_reader :desired_permissions
12
+
13
+ def initialize(name, vpc_id: nil)
14
+ error "vpc_id not specified for security group" if not vpc_id
15
+ @vpc_id = vpc_id.to_s
16
+ @name = name
17
+ @dry_run = false
18
+ end
19
+
20
+ def id
21
+ return @id unless @id.nil?
22
+ @id = load_id
23
+ end
24
+
25
+ def description(description)
26
+ @description = description
27
+ end
28
+
29
+ def id!
30
+ error "specified security_group #{@name} doesn't exist in vpc #{@vpc_id}" if not exists?
31
+ id
32
+ end
33
+
34
+ def created(&block)
35
+ clear_vars
36
+ instance_eval &block
37
+ create if not exists?
38
+ sync
39
+ end
40
+
41
+ private
42
+
43
+ def sg
44
+ @sg ||= ::Aws::EC2::SecurityGroup.new(id)
45
+ end
46
+
47
+ def create
48
+ resp = ec2.create_security_group(
49
+ group_name: @name,
50
+ description: @description,
51
+ vpc_id: @vpc_id
52
+ )
53
+ @id = resp.group_id
54
+ logger.info "(#{@name}) created security group"
55
+ rescue Aws::Errors::ServiceError, ArgumentError
56
+ error "while creating security_group #{@name}"
57
+ end
58
+
59
+ def load_id
60
+ filters = []
61
+ filters << { name: "group-name", values: [@name.to_s] }
62
+ filters << vpc_filter
63
+ result = ec2.describe_security_groups(filters: filters)
64
+ if result.security_groups.size == 1
65
+ return result.security_groups.first.group_id
66
+ else
67
+ return false
68
+ end
69
+ end
70
+
71
+ def exists?
72
+ id
73
+ end
74
+
75
+ def vpc_filter
76
+ { name: "vpc-id", values: [@vpc_id.to_s] }
77
+ end
78
+
79
+ def outbound(&block)
80
+ @outbound = true
81
+ instance_eval &block
82
+ ensure
83
+ @outbound = false
84
+ end
85
+
86
+ def parse_ports(port)
87
+ if port.is_a? Integer
88
+ return port, port
89
+ elsif port =~ /\A([0-9]+)\z/
90
+ return $1.to_i, $1.to_i
91
+ elsif port =~ /\A([0-9]+)\-([0-9]+)\z/
92
+ return $1.to_i, $2.to_i
93
+ else
94
+ error "invalid port specified #{port}"
95
+ end
96
+ end
97
+
98
+ def tcp(port, **args)
99
+ (from_port, to_port) = parse_ports(port)
100
+ rule(ip_protocol: "tcp", from_port: from_port, to_port: to_port, **args)
101
+ end
102
+
103
+ def udp(port, **args)
104
+ (from_port, to_port) = parse_ports(port)
105
+ rule(ip_protocol: "udp", from_port: from_port, to_port: to_port, **args)
106
+ end
107
+
108
+ def icmp(**args)
109
+ rule(ip_protocol: "icmp", from_port: -1, to_port: -1, **args)
110
+ end
111
+
112
+ def rule(ip_protocol: nil, from_port: nil, to_port: nil, cidr_ip: nil)
113
+ options = {
114
+ ip_protocol: ip_protocol,
115
+ from_port: from_port,
116
+ to_port: to_port,
117
+ cidr_ip: cidr_ip
118
+ }
119
+ if not @outbound
120
+ @desired_permissions ||= Set.new
121
+ @desired_permissions << options
122
+ end
123
+ end
124
+
125
+ def current_permissions
126
+ @current_permissions ||= begin
127
+ permissions = Set.new
128
+ sg.ip_permissions.each do |permission|
129
+ rules = permission_to_rules(permission)
130
+ rules.each { |r| permissions << r }
131
+ end
132
+ permissions
133
+ end
134
+ end
135
+
136
+ def diff(current, desired)
137
+ to_add = desired - current
138
+ to_remove = current - desired
139
+ s = Struct.new(:to_add, :to_remove)
140
+ s.new(to_add, to_remove)
141
+ end
142
+
143
+ def sync
144
+ ingress_diff.to_add.each { |r| authorize_ingress r }
145
+ ingress_diff.to_remove.each { |r| revoke_ingress r }
146
+ end
147
+
148
+
149
+ def authorize_ingress(rule)
150
+ sg.authorize_ingress rule if not @dry_run
151
+ logger.info "(#{@name}) #{"+".green.bold} allow #{rule[:cidr_ip]} #{rule[:from_port]}-#{rule[:to_port]}"
152
+ rescue Aws::Errors::ServiceError
153
+ error "error authorizing rule #{rule}"
154
+ end
155
+
156
+ def revoke_ingress(rule)
157
+ sg.revoke_ingress rule if not @dry_run
158
+ logger.info "(#{@name}) #{"-".red.bold} allow #{rule[:cidr_ip]} #{rule[:from_port]}-#{rule[:to_port]}"
159
+ end
160
+
161
+ def ingress_diff
162
+ @ingress_diff ||= diff(current_permissions, desired_permissions)
163
+ end
164
+
165
+ def clear_vars
166
+ @desired_permissions = nil
167
+ @current_permissions = nil
168
+ @ingress_diff = nil
169
+ end
170
+
171
+ def permission_to_rules(permission)
172
+ rules = Set.new
173
+ permission.ip_ranges.each do |r|
174
+ rule = {
175
+ ip_protocol: permission.ip_protocol,
176
+ from_port: permission.from_port,
177
+ to_port: permission.to_port,
178
+ cidr_ip: r.cidr_ip
179
+ }
180
+ rules << rule
181
+ end
182
+ rules
183
+ end
184
+
185
+ def ec2
186
+ @ec2 ||= begin
187
+ if @region
188
+ Aws::EC2::Client.new(region: @region)
189
+ else
190
+ Aws::EC2::Client.new
191
+ end
192
+ end
193
+ end
194
+
195
+ end
196
+ end
data/lib/ec2/subnet.rb ADDED
@@ -0,0 +1,105 @@
1
+ require 'ec2/helper'
2
+ require 'ec2/logger'
3
+ module Ec2
4
+ class Subnet
5
+
6
+ include Helper
7
+ include Logger
8
+
9
+ def initialize(name, vpc_id: nil)
10
+ error "vpc_id not specified for subnet" if not vpc_id
11
+ @vpc_id = vpc_id.to_s
12
+ @name = name.to_s
13
+ end
14
+
15
+ def id!
16
+ load_id if not @id
17
+ error "specified subnet #{@name} doesn't exist in vpc #{@vpc_id}" if not exists?
18
+ @id
19
+ end
20
+
21
+ def created(&block)
22
+ instance_eval &block
23
+ load_id using_cidr: true
24
+ if exists?
25
+ tag if not tagged?
26
+ verify
27
+ else
28
+ create
29
+ tag
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def subnet
36
+ @subnet ||= ::Aws::EC2::Subnet.new(@id)
37
+ end
38
+
39
+ def create
40
+ resp = ec2.create_subnet(
41
+ vpc_id: @vpc_id,
42
+ cidr_block: @cidr,
43
+ availability_zone: @availability_zone
44
+ )
45
+ @id = resp.subnet.subnet_id
46
+ logger.info "(#{@name}) created subnet: #{@cidr}"
47
+ rescue Aws::Errors::ServiceError, ArgumentError
48
+ error "while creating subnet #{@name}"
49
+ end
50
+
51
+ def tag
52
+ subnet.create_tags(
53
+ tags: [ { key: "Name", value: @name } ]
54
+ )
55
+ subnet.load
56
+ end
57
+
58
+ def tagged?
59
+ subnet.tags.any? { |tag| tag.key == "Name"}
60
+ end
61
+
62
+ def verify
63
+ name_tag = subnet.tags.find { |tag| tag.key == "Name" }.value
64
+ error "availability zone mismatch for subnet #{@name}" if subnet.availability_zone != @availability_zone
65
+ error "subnet #{@name} already tagged with another name #{name_tag}" if @name != name_tag
66
+ end
67
+
68
+ def cidr(cidr)
69
+ @cidr = cidr
70
+ end
71
+
72
+ def availability_zone(availability_zone)
73
+ @availability_zone = availability_zone
74
+ end
75
+
76
+ alias_method :az, :availability_zone
77
+
78
+
79
+ def load_id(using_cidr: false)
80
+ filters = []
81
+
82
+ if using_cidr
83
+ filters << { name: "cidr", values: [@cidr.to_s] }
84
+ else
85
+ filters << { name: "tag:Name", values: [@name.to_s] }
86
+ end
87
+
88
+ filters << { name: "vpc-id", values: [@vpc_id.to_s] }
89
+ result = ec2.describe_subnets(filters: filters)
90
+ error "mulitple subnets found with name #{@name}" if result.subnets.size > 1
91
+ @id = result.subnets.first.subnet_id if result.subnets.size == 1
92
+ end
93
+
94
+ def exists?
95
+ @id
96
+ end
97
+
98
+ def ec2
99
+ @ec2 ||= begin
100
+ @region ? Aws::EC2::Client.new(region: @region) : Aws::EC2::Client.new
101
+ end
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,3 @@
1
+ module Ec2
2
+ VERSION = "0.0.4"
3
+ end
data/lib/ec2.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "ec2/version"
2
+
3
+ module Ec2
4
+ # Your code goes here...
5
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Neeraj Bhunwal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.0.23
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.23
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: Integrates ec2 resources with salt
56
+ email:
57
+ - neeraj.bhunwal@gmail.com
58
+ executables:
59
+ - ec2
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .gitignore
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/ec2
69
+ - dev_bin/ec2
70
+ - ec2.gemspec
71
+ - examples/profiles.rb
72
+ - examples/templates.rb
73
+ - examples/test.rb
74
+ - lib/ec2.rb
75
+ - lib/ec2/cli/hosts.rb
76
+ - lib/ec2/cli/resources.rb
77
+ - lib/ec2/config.rb
78
+ - lib/ec2/custom_logger.rb
79
+ - lib/ec2/erb_profile.rb
80
+ - lib/ec2/error.rb
81
+ - lib/ec2/ext/string.rb
82
+ - lib/ec2/helper.rb
83
+ - lib/ec2/lock.rb
84
+ - lib/ec2/logger.rb
85
+ - lib/ec2/profile.rb
86
+ - lib/ec2/profile_dsl.rb
87
+ - lib/ec2/query_api.rb
88
+ - lib/ec2/resources.rb
89
+ - lib/ec2/salt_cloud.rb
90
+ - lib/ec2/security_group.rb
91
+ - lib/ec2/subnet.rb
92
+ - lib/ec2/version.rb
93
+ homepage: ''
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.0.14
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Integrates ec2 resources with salt
117
+ test_files: []