awsom 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a4b8a319b849ca7accad77d9e127a61d36601faa39185ea2b8d0afdb38a5789b
4
+ data.tar.gz: 521125b13c35ca550c9e2f73567b78a0b93a95bbbc51a5efd5d62bda6085b203
5
+ SHA512:
6
+ metadata.gz: a69c757a2bdc7b9ec6d62683020ba05e418bfd54a6a8ba4cbb26e4d4c6f2cf79f0bb7ec4ee244154ca705dd910678608e78688e7c0c89ea0c89bf3fc5b3cb3a4
7
+ data.tar.gz: 79eba4391587b74cf9acae2fcbf295968229974ad93cd63656de7b70581bf05ccb9eddab84f59146819c9b0a5219340d5ce8b3a0dfe2b2c0317c956ea3f3758f
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in awsom.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Neeraj Bhunwal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Awsom
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/awsom`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'awsom'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install awsom
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/awsom.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "awsom/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "awsom"
8
+ spec.version = Awsom::VERSION
9
+ spec.authors = ["Neeraj"]
10
+ # spec.email = ["neeraj.bhunwal@gmail.com"]
11
+
12
+ spec.summary = %q{Aws IaC dsl}
13
+ # spec.description = %q{TODO: Write a longer description or delete this line.}
14
+ # spec.homepage = "TODO: Put your gem's website or public repo URL here."
15
+ spec.license = "MIT"
16
+
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.17"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "awsom"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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__ + '/../exe/awsom'
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ Signal.trap("INT") { exit 1 }
3
+
4
+ $stdout.sync = true
5
+
6
+ require 'optparse'
7
+ require 'awsom/version'
8
+ require 'awsom/error'
9
+ require 'awsom/logger'
10
+
11
+ logger = ::Awsom::Logger.logger
12
+
13
+ options = {}
14
+ opts_parser = OptionParser.new do |opts|
15
+
16
+ banner = []
17
+ banner << "Usage: awsom [global options] command [options] args"
18
+ banner << "Commands: resources"
19
+
20
+ banner << "Options: "
21
+ opts.banner = banner.join("\n")
22
+
23
+ opts.on("-v", "--version", "Show version") do |v|
24
+ require 'awsom/version'
25
+ puts ::Awsom::VERSION
26
+ exit
27
+ end
28
+
29
+ opts.on("--debug", "Show debug messages") do
30
+ options[:debug] = true
31
+ logger.level = ::Logger::DEBUG
32
+ end
33
+
34
+ opts.on("--trace", "Show debug messages and exception stack trace") do
35
+ options[:debug] = true
36
+ options[:trace] = true
37
+ logger.level = ::Logger::DEBUG
38
+ # logger.trace_mode = true
39
+ end
40
+
41
+ opts.on_tail("-h", "--help", "Show this message") do
42
+ puts opts
43
+ exit
44
+ end
45
+ end
46
+
47
+ begin
48
+ opts_parser.order!(ARGV)
49
+ command = (ARGV.shift || '')
50
+ case command
51
+ when "resources", "r"
52
+ require 'awsom/cli/resources'
53
+ cli = ::Awsom::Cli::Resources.new(ARGV)
54
+ cli.run
55
+ when ''
56
+ puts opts_parser
57
+ else
58
+ raise ::Awsom::Error, "no such command #{command}"
59
+ end
60
+
61
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument, ::Awsom::Error => e
62
+ cause = e.cause
63
+ if options[:trace]
64
+ puts cause
65
+ cause ? (raise cause) : (raise e)
66
+ else
67
+ logger.debug "#{cause.message}" if cause
68
+ logger.error "#{e.message}"
69
+ abort
70
+ end
71
+ end
@@ -0,0 +1,6 @@
1
+ require "awsom/version"
2
+
3
+ module Awsom
4
+ class Error < StandardError; end
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'security_group'
2
+ require_relative 'subnet'
3
+ require_relative 'instance'
4
+ require_relative 'instance_dsl'
5
+
6
+ module Awsom
7
+ class Application
8
+
9
+ def initialize(vpc_id:, region:)
10
+ @vpc_id = vpc_id
11
+ @region = region
12
+ end
13
+
14
+ def run(files)
15
+ files.each do |f|
16
+ apply f
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def security_group(name, &block)
23
+ sg = SecurityGroup.new(name, vpc_id: @vpc_id)
24
+ sg.created(&block)
25
+ end
26
+
27
+ def subnet(name, az:, cidr:, &block)
28
+ az = "#{@region}#{az}"
29
+ subnet = Subnet.new(name, cidr: cidr, vpc_id: @vpc_id, az: az)
30
+ id = subnet.created
31
+ puts "subnet #{name} (#{cidr}) #{id}"
32
+ @subnet_id = id
33
+ instance_eval(&block) if block
34
+ ensure
35
+ @subnet_id = nil
36
+ end
37
+
38
+ def instance(name, &block)
39
+ instance = Instance.new(name, vpc_id: @vpc_id)
40
+ dsl = InstanceDSL.new(name, vpc_id: @vpc_id, subnet_id: @subnet_id)
41
+ dsl.read(&block)
42
+ setup_proc = dsl.setup_proc
43
+ instance.created(setup_proc) { dsl.run_params }
44
+ end
45
+
46
+ def apply(file)
47
+ instance_eval(File.read(file), file)
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,59 @@
1
+ require 'awsom/lock'
2
+ require 'awsom/config'
3
+
4
+ module Awsom
5
+ module Cli
6
+ class Resources
7
+
8
+ def initialize(argv)
9
+ @argv = argv
10
+ end
11
+
12
+ def run
13
+ lock.acquire
14
+ opts.parse!(@argv)
15
+ config = Config.new("config.rb").config
16
+
17
+ init_aws(config)
18
+
19
+ vpc_id = config.fetch :vpc_id
20
+ region = config.fetch :region
21
+
22
+ require 'awsom/application'
23
+ app = Application.new(vpc_id: vpc_id, region: region)
24
+ app.run(@argv)
25
+ end
26
+
27
+ def init_aws(config)
28
+ require 'aws-sdk-core'
29
+ if config.key?(:aws_key) && config.key?(:aws_secret)
30
+ credentials = Aws::Credentials.new(config.fetch(:aws_key), config.fetch(:aws_secret))
31
+ Aws.config[:credentials] = credentials
32
+ end
33
+ Aws.config[:region] = config.fetch(:region)
34
+ require_relative '../ec2'
35
+ end
36
+
37
+ def lock
38
+ @lock ||= ::Awsom::Lock.new
39
+ end
40
+
41
+ def opts
42
+ OptionParser.new do |opts|
43
+
44
+ opts.banner = "Usage: awsom create [options] [target]"
45
+
46
+ opts.on("-f", "--file FILE", "Specify hosts file") do |f|
47
+ salt.hosts_file = f
48
+ end
49
+
50
+ opts.on("-h", "--help", "Help") do
51
+ puts opts
52
+ exit
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,59 @@
1
+ module Awsom
2
+ class Config
3
+
4
+ def initialize(config_path)
5
+ @config_path = config_path
6
+ end
7
+
8
+ def config
9
+ @config = {
10
+ minion_config: {}
11
+ }
12
+ read_config
13
+ @config
14
+ end
15
+
16
+ private
17
+
18
+ def region(region)
19
+ set :region, region
20
+ end
21
+
22
+ def vpc_id(vpc)
23
+ set :vpc_id, vpc
24
+ end
25
+
26
+ def minion_config(config)
27
+ set :minion_config, config
28
+ end
29
+
30
+ def aws_key(aws_key)
31
+ set :aws_key, aws_key
32
+ set :use_iam, false
33
+ end
34
+
35
+ def aws_secret(aws_secret)
36
+ set :aws_secret, aws_secret
37
+ set :use_iam, false
38
+ end
39
+
40
+ def set(key, value)
41
+ @config.store key, value
42
+ end
43
+
44
+ def read_config
45
+ read(@config_path, required: false)
46
+ end
47
+
48
+ def read(file, required: true)
49
+ if not File.readable? file
50
+ raise Error, "config: #{file} not readable" if required
51
+ return
52
+ end
53
+ instance_eval(File.read(file), file)
54
+ rescue NoMethodError => e
55
+ raise Error, "invalid option used in config: #{e.name}"
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'ext/string'
2
+ module Awsom
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,4 @@
1
+ require 'aws-sdk-ec2'
2
+ module Awsom
3
+ Ec2 = Aws::EC2::Client.new
4
+ end
@@ -0,0 +1,4 @@
1
+ module Awsom
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
@@ -0,0 +1,21 @@
1
+ require_relative 'ec2'
2
+ module Awsom
3
+ module Image
4
+
5
+ @cache = {}
6
+
7
+ def self.block_device_mappings(image_id)
8
+ image = describe(image_id)
9
+ image.block_device_mappings
10
+ end
11
+
12
+ def self.describe(image_id)
13
+ if image = @cache[image_id]
14
+ return image
15
+ else
16
+ @cache[image_id] = Ec2.describe_images(image_ids: [image_id]).images[0]
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ require 'pty'
2
+ module Awsom
3
+ class Instance
4
+
5
+ include Logger
6
+
7
+ def initialize(name, vpc_id:, setup_proc: nil)
8
+ @name = name
9
+ @vpc_id = vpc_id
10
+ end
11
+
12
+ def created(setup_proc = nil, &block)
13
+ id = find_id
14
+ return id if id
15
+ run_params = yield
16
+ id = create(run_params)
17
+ instance = Aws::EC2::Instance.new(id)
18
+ puts "(#{@name}) waiting for instance to start"
19
+ instance.wait_until_running
20
+ setup_proc.call(instance) if setup_proc
21
+ end
22
+
23
+ private
24
+
25
+ def find_id
26
+ filters = [
27
+ { name: "vpc-id", values: [@vpc_id] },
28
+ { name: "tag:Name", values: [@name] }
29
+ ]
30
+
31
+ result = Ec2.describe_instances(filters: filters)
32
+ instances = []
33
+ result.reservations.each do |r|
34
+ r.instances.each { |i| instances << i }
35
+ end
36
+
37
+ num_results = instances.size
38
+ raise Error, "mulitple instances found with name #{@name}" if num_results > 1
39
+ if num_results == 1
40
+ instances.first.instance_id
41
+ else
42
+ false
43
+ end
44
+ end
45
+
46
+ def create(run_params)
47
+ result = Ec2.run_instances(run_params)
48
+ result.instances[0].instance_id
49
+ rescue Aws::Errors::ServiceError, ArgumentError
50
+ raise Error, "while creating instance #{@name}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,139 @@
1
+ require_relative 'image'
2
+ module Awsom
3
+ class InstanceDSL
4
+
5
+ attr_reader :setup_proc
6
+
7
+ def initialize(name, subnet_id:, vpc_id:)
8
+ @name = name
9
+ @subnet_id = subnet_id
10
+ @vpc_id = vpc_id
11
+ @volumes = {}
12
+ @security_groups = []
13
+ @attrs = {
14
+ image_id: nil,
15
+ key_name: nil,
16
+ size: nil,
17
+ associate_public_ip_address: true,
18
+ credits: "unlimited",
19
+ term_protect: false,
20
+ }
21
+ @setup_proc = nil
22
+ end
23
+
24
+ def read(&block)
25
+ instance_eval(&block)
26
+ end
27
+
28
+ def run_params
29
+ params
30
+ end
31
+
32
+ def include_config(pr)
33
+ instance_eval(&pr) if pr
34
+ end
35
+
36
+ private
37
+
38
+ def set(key, val)
39
+ raise Error, "unknown attr #{key}" if not @attrs.key?(key)
40
+ @attrs[key] = val
41
+ end
42
+
43
+ def get(key)
44
+ val = @attrs[key]
45
+ raise Error, "key #{key} not found" if val.nil?
46
+ val
47
+ end
48
+
49
+ def setup(&block)
50
+ @setup_proc = block
51
+ end
52
+
53
+ def setup_salt(template, &block)
54
+ require_relative 'salt_bootstrap'
55
+ @setup_proc = Proc.new do |instance|
56
+ ip = instance.private_ip_address
57
+ bootstrap = SaltBootstrap.new(@name, ip: ip, template: template)
58
+ bootstrap.run
59
+ end
60
+ end
61
+
62
+ def image_id(val)
63
+ set :image_id, val
64
+ end
65
+
66
+ def associate_public_ip_address(val)
67
+ set :associate_public_ip_address, val
68
+ end
69
+
70
+ def key_name(val)
71
+ set :key_name, val
72
+ end
73
+
74
+ def size(val)
75
+ set :size, val
76
+ end
77
+
78
+ def volume(name, size:, **opts)
79
+ ebs = { delete_on_termination: false, volume_type: "gp2", iops: nil }
80
+ ebs[:volume_size] = size
81
+ ebs.merge!(opts)
82
+ @volumes[name] = { device_name: name, ebs: ebs }
83
+ end
84
+
85
+ def security_groups(val)
86
+ @security_groups = Set.new(val)
87
+ end
88
+
89
+ def load_ami_volumes
90
+ Image.block_device_mappings(get :image_id).each do |m|
91
+ name = m.device_name
92
+ ebs = m.ebs.to_h
93
+ if @volumes.key?(name)
94
+ @volumes[name][:ebs].merge!(ebs) { |k, old, new| old }
95
+ else
96
+ @volumes[name] = { device_name: name, ebs: ebs }
97
+ end
98
+ end
99
+ end
100
+
101
+ def network_interfaces
102
+ groups = @security_groups.map do |i|
103
+ SecurityGroup.id(i, @vpc_id)
104
+ end
105
+
106
+ netif = {
107
+ associate_public_ip_address: get(:associate_public_ip_address),
108
+ delete_on_termination: true,
109
+ device_index: 0,
110
+ groups: groups,
111
+ subnet_id: @subnet_id
112
+ }
113
+ [netif]
114
+ end
115
+
116
+ def params
117
+ load_ami_volumes
118
+ tag_specifications = [
119
+ { resource_type: "instance", tags: [ { key: "Name", value: @name } ] },
120
+ { resource_type: "volume", tags: [ { key: "Name", value: @name } ] }
121
+ ]
122
+
123
+ params = {
124
+ image_id: get(:image_id),
125
+ instance_type: get(:size),
126
+ key_name: get(:key_name),
127
+ disable_api_termination: get(:term_protect),
128
+ block_device_mappings: @volumes.values,
129
+ network_interfaces: network_interfaces,
130
+ tag_specifications: tag_specifications,
131
+ min_count: 1,
132
+ max_count: 1
133
+ }
134
+
135
+ params[:credit_specification] = { cpu_credits: get(:credits) } if get(:size) =~ /\At[23]/
136
+ params
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'logger'
2
+ module Awsom
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(".awsom.lock", "a+")
15
+ end
16
+
17
+ end
18
+ end
19
+
@@ -0,0 +1,27 @@
1
+ require 'logger'
2
+ require_relative 'custom_logger'
3
+ module Awsom
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
+ ::Awsom::Logger.logger
16
+ end
17
+
18
+ def stderr
19
+ ::Awsom::Logger.stderr
20
+ end
21
+
22
+ def debug?
23
+ logger.level == ::Logger::DEBUG
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,88 @@
1
+ require 'tmpdir'
2
+ require 'yaml'
3
+ require 'pty'
4
+ require 'erb'
5
+ require 'fileutils'
6
+
7
+ module Awsom
8
+ class SaltBootstrap
9
+
10
+ def initialize(name, ip:, template:)
11
+ @name = name
12
+ @ip = ip
13
+ @template = template
14
+ validate!
15
+ end
16
+
17
+ def run
18
+ copy_bootstrap_data
19
+ puts "running bootstrap.sh"
20
+ run_bootstrap_sh
21
+ end
22
+
23
+ private
24
+
25
+ def copy_bootstrap_data
26
+ Dir.mktmpdir("./") do |dir|
27
+ puts "generating salt key"
28
+ system "salt-key", "--gen-keys-dir=#{dir}", "--gen-keys=minion"
29
+
30
+ puts "generating bootstrap script"
31
+ erb = ERB.new(File.read @template)
32
+ script = erb.result_with_hash({name: @name})
33
+ File.write("#{dir}/bootstrap.sh", script)
34
+
35
+ scp "#{dir}/minion.pem", "#{dir}/minion.pub", "#{dir}/bootstrap.sh", "root@#{@ip}:/root/"
36
+ FileUtils.mv "#{dir}/minion.pub", "/etc/salt/pki/master/minions/#{@name}"
37
+ end
38
+ end
39
+
40
+ def run_bootstrap_sh
41
+ FileUtils.mkdir_p "log"
42
+ log_file = "log/#{@name}.log"
43
+ script = 'bash /root/bootstrap.sh; rm /root/bootstrap.sh'
44
+ r, w, pid = PTY.spawn "ssh", "-tt", *ssh_opts, "root@#{@ip}", script
45
+ File.open log_file, "w" do |f|
46
+ pty_read(r, f)
47
+ end
48
+ pid, status = Process.waitpid2(pid)
49
+ p status
50
+ end
51
+
52
+ def pty_read(io, out)
53
+ io.each { |line| out.print line }
54
+ rescue EOFError,Errno::ECONNRESET, Errno::EPIPE, Errno::EIO => e
55
+ end
56
+
57
+ def scp(*args, tries: 5)
58
+ begin
59
+ tries = tries - 1
60
+ system "timeout", "60", "scp", *ssh_opts, *args, exception: true
61
+ rescue RuntimeError
62
+ sleep 5
63
+ retry if tries > 0
64
+ end
65
+ end
66
+
67
+ def ssh_opts(attempts = 65)
68
+ [
69
+ "-oConnectTimeout=1",
70
+ "-oConnectionAttempts=#{attempts}",
71
+ "-oStrictHostKeyChecking=no",
72
+ "-oUserKnownHostsFile=/dev/null",
73
+ "-oBatchMode=yes"
74
+ ]
75
+ end
76
+
77
+ def validate!
78
+ if @name !~ /\A[a-zA-Z0-9\-\. ]+\z/
79
+ raise "invalid name #{@name}"
80
+ end
81
+
82
+ if @ip !~ /\A[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\z/
83
+ raise "invalid ip #{@ip}"
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,159 @@
1
+ require_relative 'logger'
2
+ require 'set'
3
+
4
+ module Awsom
5
+ class SecurityGroup
6
+
7
+ include Logger
8
+
9
+ DiffStruct = Struct.new(:to_add, :to_remove, keyword_init: true)
10
+
11
+ def self.id(name, vpc_id)
12
+ sg = new(name, vpc_id: vpc_id)
13
+ sg.id
14
+ end
15
+
16
+ def initialize(name, vpc_id:)
17
+ @name = name
18
+ @vpc_id = vpc_id
19
+ end
20
+
21
+ def id
22
+ id = find_id
23
+ raise Error, "specified security_group #{@name} doesn't exist in vpc #{@vpc_id}" if not id
24
+ id
25
+ end
26
+
27
+ def description(description)
28
+ @description = description
29
+ end
30
+
31
+ def created(&block)
32
+ @desired_rules = Set.new
33
+ instance_eval &block
34
+ @id = find_id || create
35
+ sync
36
+ @id
37
+ end
38
+
39
+ private
40
+
41
+ def sg
42
+ @sg ||= Aws::EC2::SecurityGroup.new(id)
43
+ end
44
+
45
+ def find_id
46
+ filters = [
47
+ { name: "group-name", values: [@name] },
48
+ { name: "vpc-id", values: [@vpc_id] }
49
+ ]
50
+ result = Ec2.describe_security_groups(filters: filters)
51
+ num_results = result.security_groups.size
52
+ if num_results == 1
53
+ result.security_groups.first.group_id
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def create
60
+ resp = Ec2.create_security_group(
61
+ group_name: @name,
62
+ description: @description,
63
+ vpc_id: @vpc_id
64
+ )
65
+ logger.info "(#{@name}) created security group"
66
+ resp.group_id
67
+ rescue Aws::Errors::ServiceError, ArgumentError
68
+ error "while creating security_group #{@name}"
69
+ end
70
+
71
+ def parse_ports(port)
72
+ case port
73
+ when Integer
74
+ [port, port]
75
+ when Range
76
+ [port.first, port.last]
77
+ when /\A([0-9]+)\z/
78
+ [$1.to_i, $1.to_i]
79
+ when /\A([0-9]+)\-([0-9]+)\z/
80
+ [$1.to_i, $2.to_i]
81
+ else
82
+ raise Error, "invalid port specified #{port}"
83
+ end
84
+ end
85
+
86
+ def tcp(port, cidr_ip:)
87
+ (from_port, to_port) = parse_ports(port)
88
+ rule(ip_protocol: "tcp", from_port: from_port, to_port: to_port, cidr_ip: cidr_ip)
89
+ end
90
+
91
+ def udp(port, cidr_ip:)
92
+ (from_port, to_port) = parse_ports(port)
93
+ rule(ip_protocol: "udp", from_port: from_port, to_port: to_port, cidr_ip: cidr_ip)
94
+ end
95
+
96
+ def icmp(cidr_ip)
97
+ rule(ip_protocol: "icmp", from_port: -1, to_port: -1, cidr_ip: cidr_ip)
98
+ end
99
+
100
+ def rule(ip_protocol:, from_port:, to_port:, cidr_ip:)
101
+ rule = {
102
+ ip_protocol: ip_protocol,
103
+ from_port: from_port,
104
+ to_port: to_port,
105
+ cidr_ip: cidr_ip
106
+ }
107
+ @desired_rules << rule
108
+ end
109
+
110
+ def current_rules
111
+ current_rules = Set.new
112
+ sg.ip_permissions.each do |permission|
113
+ rules = permission_to_rules(permission)
114
+ rules.each { |r| current_rules << r }
115
+ end
116
+ current_rules
117
+ end
118
+
119
+ def diff(current, desired)
120
+ to_add = desired - current
121
+ to_remove = current - desired
122
+ DiffStruct.new(to_add: to_add, to_remove: to_remove)
123
+ end
124
+
125
+ def sync
126
+ current = current_rules
127
+ ingress_diff = diff(current, @desired_rules)
128
+ ingress_diff.to_add.each { |r| authorize_ingress r }
129
+ ingress_diff.to_remove.each { |r| revoke_ingress r }
130
+ end
131
+
132
+ def authorize_ingress(rule)
133
+ sg.authorize_ingress rule if not @dry_run
134
+ logger.info "(#{@name}) #{"+".green.bold} allow #{rule[:cidr_ip]} #{rule[:from_port]}-#{rule[:to_port]}"
135
+ rescue Aws::Errors::ServiceError
136
+ error "error authorizing rule #{rule}"
137
+ end
138
+
139
+ def revoke_ingress(rule)
140
+ sg.revoke_ingress rule if not @dry_run
141
+ logger.info "(#{@name}) #{"-".red.bold} allow #{rule[:cidr_ip]} #{rule[:from_port]}-#{rule[:to_port]}"
142
+ end
143
+
144
+ def permission_to_rules(permission)
145
+ rules = Set.new
146
+ permission.ip_ranges.each do |r|
147
+ rule = {
148
+ ip_protocol: permission.ip_protocol,
149
+ from_port: permission.from_port,
150
+ to_port: permission.to_port,
151
+ cidr_ip: r.cidr_ip
152
+ }
153
+ rules << rule
154
+ end
155
+ rules
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,92 @@
1
+ require_relative 'logger'
2
+
3
+ module Awsom
4
+ class Subnet
5
+
6
+ include Logger
7
+
8
+ def initialize(name, cidr:, az:, vpc_id:)
9
+ @vpc_id = vpc_id
10
+ @name = name
11
+ @cidr = cidr
12
+ @az = az
13
+ end
14
+
15
+ def id
16
+ id = find_id
17
+ error "specified subnet #{@name} doesn't exist in vpc #{@vpc_id}" if not id
18
+ id
19
+ end
20
+
21
+ def created
22
+ id = find_id
23
+ if id
24
+ subnet = subnet(id)
25
+ tag(subnet) if not tagged?(subnet)
26
+ return id
27
+ end
28
+ create
29
+ end
30
+
31
+ private
32
+
33
+ def create
34
+ resp = Ec2.create_subnet(
35
+ vpc_id: @vpc_id,
36
+ cidr_block: @cidr,
37
+ availability_zone: @availability_zone
38
+ )
39
+ logger.info "(#{@name}) created subnet: #{@cidr}"
40
+ id = resp.subnet.subnet_id
41
+ subnet = subnet(id)
42
+ tag(subnet)
43
+ id
44
+ rescue Aws::Errors::ServiceError, ArgumentError
45
+ raise Error, "while creating subnet #{@name}"
46
+ end
47
+
48
+ def subnet(id)
49
+ Aws::EC2::Subnet.new(id)
50
+ end
51
+
52
+ def tag(subnet)
53
+ return if tagged?(subnet)
54
+ subnet.create_tags(
55
+ tags: [ { key: "Name", value: @name } ]
56
+ )
57
+ subnet.load
58
+ end
59
+
60
+ def tagged?(subnet)
61
+ subnet.tags.any? { |tag| tag.key == "Name"}
62
+ end
63
+
64
+ def verify(subnet)
65
+ error "availability zone mismatch for subnet #{@name}" if subnet.availability_zone != @az
66
+ name_tag = subnet.tags.find { |tag| tag.key == "Name" }
67
+ return if not name_tag
68
+ if name_tag.value != @name
69
+ raise Error, "subnet #{@name} already tagged with another name #{name_tag}"
70
+ end
71
+ end
72
+
73
+ def find_id
74
+ filters = [
75
+ { name: "vpc-id", values: [@vpc_id.to_s] },
76
+ { name: "cidr", values: [@cidr.to_s] }
77
+ ]
78
+
79
+ result = Ec2.describe_subnets(filters: filters)
80
+ num_results = result.subnets.size
81
+ raise Error, "mulitple subnets found with name #{@name}" if num_results > 1
82
+ if num_results == 1
83
+ subnet = result.subnets.first
84
+ verify(subnet)
85
+ result.subnets.first.subnet_id
86
+ else
87
+ false
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module Awsom
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,85 @@
1
+ require_relative 'logger'
2
+
3
+ module Awsom
4
+ class Vpc
5
+
6
+ include Logger
7
+
8
+ def initialize(name, cidr:)
9
+ @name = name
10
+ @cidr = cidr
11
+ end
12
+
13
+ def id
14
+ id = find_id
15
+ error "specified vpc #{@name} doesn't exist" if not id
16
+ id
17
+ end
18
+
19
+ def created
20
+ id = find_id
21
+ if id
22
+ vpc = vpc(id)
23
+ tag(vpc) if not tagged?(vpc)
24
+ id
25
+ else
26
+ create
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def create
33
+ resp = Ec2.create_vpc(cidr_block: @cidr)
34
+ logger.info "(#{@name}) created vpc: #{@cidr}"
35
+ id = resp.vpc.vpc_id
36
+ vpc = vpc(id)
37
+ tag(vpc)
38
+ id
39
+ rescue Aws::Errors::ServiceError, ArgumentError
40
+ raise Error, "while creating vpc #{@name}"
41
+ end
42
+
43
+ def vpc(id)
44
+ Aws::EC2::Vpc.new(id)
45
+ end
46
+
47
+ def tag(vpc)
48
+ return if tagged?(vpc)
49
+ vpc.create_tags(
50
+ tags: [ { key: "Name", value: @name } ]
51
+ )
52
+ vpc.load
53
+ end
54
+
55
+ def tagged?(vpc)
56
+ vpc.tags.any? { |tag| tag.key == "Name"}
57
+ end
58
+
59
+ def verify(vpc)
60
+ name_tag = vpc.tags.find { |tag| tag.key == "Name" }
61
+ return if not name_tag
62
+ if name_tag.value != @name
63
+ raise Error, "vpc #{@name} already tagged with another name #{name_tag}"
64
+ end
65
+ end
66
+
67
+ def find_id
68
+ filters = [
69
+ { name: "cidr", values: [@cidr.to_s] }
70
+ ]
71
+
72
+ result = Ec2.describe_vpcs(filters: filters)
73
+ num_results = result.vpcs.size
74
+ raise Error, "mulitple vpcs found for name #{@name}" if num_results > 1
75
+ if num_results == 1
76
+ vpc = result.vpcs.first
77
+ verify(vpc)
78
+ result.vpc.first.vpc_id
79
+ else
80
+ false
81
+ end
82
+ end
83
+
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: awsom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Neeraj
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-05-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.17'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description:
42
+ email:
43
+ executables:
44
+ - awsom
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - awsom.gemspec
54
+ - bin/console
55
+ - bin/setup
56
+ - dev_exe/awsom
57
+ - exe/awsom
58
+ - lib/awsom.rb
59
+ - lib/awsom/application.rb
60
+ - lib/awsom/cli/resources.rb
61
+ - lib/awsom/config.rb
62
+ - lib/awsom/custom_logger.rb
63
+ - lib/awsom/ec2.rb
64
+ - lib/awsom/error.rb
65
+ - lib/awsom/ext/string.rb
66
+ - lib/awsom/image.rb
67
+ - lib/awsom/instance.rb
68
+ - lib/awsom/instance_dsl.rb
69
+ - lib/awsom/lock.rb
70
+ - lib/awsom/logger.rb
71
+ - lib/awsom/salt_bootstrap.rb
72
+ - lib/awsom/security_group.rb
73
+ - lib/awsom/subnet.rb
74
+ - lib/awsom/version.rb
75
+ - lib/awsom/vpc.rb
76
+ homepage:
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.0.3
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Aws IaC dsl
99
+ test_files: []