aws-must 0.0.2

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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +235 -0
  3. data/bin/aws-must.rb +92 -0
  4. data/demo/0/conf.yaml +1 -0
  5. data/demo/0/root.mustache +1 -0
  6. data/demo/1/conf.yaml +1 -0
  7. data/demo/1/root.mustache +22 -0
  8. data/demo/2/conf.yaml +6 -0
  9. data/demo/2/root.mustache +54 -0
  10. data/demo/3/conf.yaml +3 -0
  11. data/demo/3/resources.mustache +26 -0
  12. data/demo/3/root.mustache +85 -0
  13. data/demo/4/conf.yaml +22 -0
  14. data/demo/4/resource.mustache +27 -0
  15. data/demo/4/resourceInstance.mustache +32 -0
  16. data/demo/4/resources.mustache +25 -0
  17. data/demo/4/root.mustache +95 -0
  18. data/demo/5/conf.yaml +32 -0
  19. data/demo/5/output.mustache +39 -0
  20. data/demo/5/resource.mustache +26 -0
  21. data/demo/5/resourceInstance.mustache +35 -0
  22. data/demo/5/resources.mustache +25 -0
  23. data/demo/5/root.mustache +100 -0
  24. data/demo/6/conf.yaml +31 -0
  25. data/demo/6/mappings.mustache +67 -0
  26. data/demo/6/output.mustache +40 -0
  27. data/demo/6/resource.mustache +29 -0
  28. data/demo/6/resourceInstance.mustache +35 -0
  29. data/demo/6/resources.mustache +35 -0
  30. data/demo/6/root.mustache +133 -0
  31. data/demo/7/conf.yaml +54 -0
  32. data/demo/7/mappings.mustache +67 -0
  33. data/demo/7/output.mustache +40 -0
  34. data/demo/7/parameter.mustache +38 -0
  35. data/demo/7/resource.mustache +38 -0
  36. data/demo/7/resourceInstance.mustache +45 -0
  37. data/demo/7/resourceSecurityGroup.mustache +37 -0
  38. data/demo/7/resources.mustache +35 -0
  39. data/demo/7/root.mustache +157 -0
  40. data/demo/7/tag.mustache +30 -0
  41. data/lib/aws-must/aws-must.rb +96 -0
  42. data/lib/aws-must/docu.rb +164 -0
  43. data/lib/aws-must/template.rb +81 -0
  44. data/lib/aws-must.rb +11 -0
  45. data/lib/tasks/demo.rake +139 -0
  46. data/lib/utils/hasher.rb +83 -0
  47. data/lib/utils/logger.rb +71 -0
  48. metadata +106 -0
@@ -0,0 +1,96 @@
1
+ require 'yaml'
2
+ require 'json'
3
+
4
+ module AwsMust
5
+
6
+ class AwsMust
7
+
8
+ include Utils::MyLogger # mix logger
9
+ PROGNAME = "aws-must" # logger progname
10
+ include Utils::Hasher # mix logger
11
+
12
+ # ------------------------------------------------------------------
13
+ # attributes
14
+
15
+ attr_accessor :template # mustache (or something else)
16
+
17
+ def initialize( options={} )
18
+
19
+ # init logger
20
+ @logger = getLogger( PROGNAME, options )
21
+ @logger.info( "#{__method__}" )
22
+
23
+ # create object, which knows how to generate CloudFormation template json
24
+ @template = Template.new( options )
25
+ @docu = Docu.new( @template, options )
26
+ end
27
+
28
+ # ------------------------------------------------------------------
29
+ # dump 'yaml_file' as json
30
+
31
+ def json( yaml_file )
32
+
33
+ @logger.debug( "#{__method__}, template_name '#{yaml_file}'" )
34
+
35
+ #
36
+ data = read_yaml_file( yaml_file )
37
+
38
+ puts data.to_json
39
+
40
+
41
+ end #
42
+
43
+ # ------------------------------------------------------------------
44
+ # extract documentation from 'template_name'
45
+
46
+ def doc( template_name )
47
+
48
+ @logger.debug( "#{__method__}, template_name '#{template_name}'" )
49
+
50
+ @docu.document( template_name )
51
+
52
+
53
+ end
54
+
55
+ # ------------------------------------------------------------------
56
+ # generate JSON by rendering 'template_name' using configurations
57
+ # from 'yaml_file'
58
+
59
+ def generate( template_name, yaml_file, options )
60
+
61
+ @logger.debug( "#{__method__}, template_name '#{template_name}'" )
62
+ @logger.debug( "#{__method__}, yaml_file= '#{yaml_file}'" )
63
+
64
+ #
65
+ data = read_yaml_file( yaml_file )
66
+
67
+ # some 'quirks' on data
68
+ data = adjust( data )
69
+ @logger.debug( "#{__method__}, data= '#{data}'" )
70
+
71
+ puts template.to_str( template_name, data )
72
+
73
+ end
74
+
75
+ private
76
+
77
+ # deeply iterate 'data' -hash and add comma (,) to all expect the
78
+ # last hash in arrays
79
+ def adjust( data )
80
+ return addComma( data )
81
+ end
82
+
83
+
84
+ def read_yaml_file( yaml_file )
85
+
86
+ raise "YAML file #{yaml_file} does not exist" unless File.exist?( yaml_file )
87
+ data = YAML.load_file( yaml_file )
88
+
89
+ return data
90
+
91
+ end
92
+
93
+
94
+ end
95
+
96
+ end
@@ -0,0 +1,164 @@
1
+
2
+ module AwsMust
3
+
4
+ class Docu
5
+
6
+ include ::Utils::MyLogger # mix logger
7
+ PROGNAME = "docu" # progname for logger
8
+
9
+ # ------------------------------------------------------------------
10
+ # constants
11
+
12
+ DEFAULT_OPEN_TAG ="++start++"
13
+ DEFAULT_CLOSE_TAG ="++close++"
14
+ DEFAULT_INCLUDE_TAG =">"
15
+
16
+
17
+ # ------------------------------------------------------------------
18
+ # Attributes
19
+
20
+ # static
21
+
22
+
23
+ # instance
24
+
25
+ # ------------------------------------------------------------------
26
+ # Constructor
27
+ # - template: which knows how to return template files
28
+
29
+ def initialize( template, options={} )
30
+
31
+ @logger = getLogger( PROGNAME, options )
32
+ @logger.info( "#{__method__}, options=#{options}" )
33
+
34
+ @template = template
35
+
36
+ @directives = {
37
+ :open => DEFAULT_OPEN_TAG,
38
+ :close => DEFAULT_CLOSE_TAG,
39
+ :include => DEFAULT_INCLUDE_TAG,
40
+ }
41
+
42
+ end
43
+
44
+ # ------------------------------------------------------------------
45
+ # docu - output documentation to stdout
46
+
47
+ def document( template_name )
48
+
49
+ @logger.debug( "#{__method__}, template_name '#{template_name}'" )
50
+
51
+ #
52
+ template_string = @template.get_template( template_name )
53
+ @logger.debug( "#{__method__}, template_string= '#{template_string}'" )
54
+
55
+ #
56
+ state_opened=false
57
+
58
+ # iterate lines
59
+ template_string && template_string.split( "\n" ).each do | line |
60
+
61
+ if !state_opened then
62
+
63
+ # waiting for +++open+++
64
+
65
+ open_found, line = directive_open( line )
66
+ if open_found then
67
+
68
+ state_opened = true
69
+
70
+ # something follows the start -tag
71
+ redo if line && ! line.empty?
72
+
73
+ end
74
+
75
+ else
76
+
77
+ # seen +++open+++, waiting for +++close+++
78
+
79
+ close_found, line_pre, line = directive_close( line )
80
+ # puts "close_found=#{close_found}, #{close_found.class}, line=#{line}"
81
+ if close_found then
82
+ output( line_pre ) if line_pre && !line_pre.empty?
83
+ state_opened = false
84
+ redo if line && ! line.empty?
85
+ elsif template_name = directive_include( line ) then
86
+ document( template_name )
87
+ else
88
+ output( line )
89
+ end
90
+
91
+ end # state_opened
92
+ end # each line
93
+
94
+ end # end - document
95
+
96
+ # ------------------------------------------------------------------
97
+ # output - document output
98
+
99
+ def output( line )
100
+
101
+ puts( line )
102
+ end
103
+
104
+
105
+ # ------------------------------------------------------------------
106
+ # Check if line satisfies tag
107
+
108
+ def directive_open( line )
109
+ # return line && line.include?( get_tag(:open) )
110
+ if line && line.include?( get_tag(:open) ) then
111
+ # find index pointing _after_ :open tag
112
+ index = line.index( get_tag(:open) ) + get_tag(:open).length
113
+ # # return text after directtive
114
+ # return true, line[ index..-1]
115
+
116
+ # do not output anything for the line
117
+ return true, nil
118
+
119
+ else
120
+ return false, nil
121
+ end
122
+ end
123
+
124
+ # return bool, line_pre, line
125
+ def directive_close( line )
126
+ #return line && line.include?( get_tag( :close ) )
127
+ if line && line.include?( get_tag( :close ) ) then
128
+ index = line.index( get_tag(:close) )
129
+ # return true, index == 0 ? nil : line[0..index-1], line[ index+ get_tag(:close).length..-1]
130
+ # do not output anythin on line for directive
131
+ return true, nil, nil
132
+ else
133
+ return false, nil, line
134
+ end
135
+ end
136
+
137
+
138
+ # return include 'template' to include if it is include directive
139
+ def directive_include( line )
140
+
141
+ # not valid input
142
+ return nil unless line
143
+
144
+ # extract template_name using regexp
145
+ parse_regexp=/^#{get_tag(:include)}\s+(?<template>[\w]*)\s*/
146
+ parsed = line.match( parse_regexp )
147
+
148
+ # not found
149
+ return nil unless parsed
150
+ return parsed['template']
151
+
152
+ end
153
+
154
+ # ------------------------------------------------------------------
155
+ # get tag value
156
+ def get_tag( tag )
157
+ raise "Unknown tag" unless @directives[tag]
158
+ return @directives[tag]
159
+ end
160
+
161
+
162
+ end # class
163
+
164
+ end # module
@@ -0,0 +1,81 @@
1
+
2
+ require 'mustache' # extendending implementation of
3
+
4
+
5
+ module AwsMust
6
+
7
+ class Template < Mustache
8
+
9
+ include ::Utils::MyLogger # mix logger
10
+ PROGNAME = "template" # progname for logger
11
+
12
+ # ------------------------------------------------------------------
13
+ # Attributes
14
+
15
+ # static
16
+ @@template_path = nil # directory where templates stored
17
+ @@template_extension = "mustache"# type part in filename
18
+
19
+ # instance
20
+ attr_writer :partials # f: partial-name --> template string
21
+ attr_writer :templates # f: template-name --> template string
22
+
23
+ # ------------------------------------------------------------------
24
+ # Constructor
25
+
26
+ def initialize( options={} )
27
+ @logger = getLogger( PROGNAME, options )
28
+ @logger.info( "#{__FILE__}.#{__method__} created" )
29
+ @logger.debug( "#{__FILE__}.#{__method__}, options='#{options}" )
30
+ # for mustache templates
31
+ @@template_path = options[:template_path] if options[:template_path]
32
+ end
33
+
34
+ # ------------------------------------------------------------------
35
+ # Services
36
+
37
+ def to_str( template_name, data )
38
+ @logger.debug( "#{__method__}: template_name=#{template_name}, data=#{data}" )
39
+ if template_name
40
+ template = get_template( template_name )
41
+ render( template, data )
42
+ else
43
+ render( data )
44
+ end
45
+ end
46
+
47
+ # ------------------------------------------------------------------
48
+ # Integrate with mustache
49
+
50
+ # method used by mustache framework - delegate to 'get_partial'
51
+ def partial(name)
52
+ @logger.debug( "#{__FILE__}.#{__method__} name=#{name}" )
53
+ # File.read("#{template_path}/#{name}.#{template_extension}")
54
+ get_partial( name )
55
+ end
56
+
57
+ # cache @partials - for easier extension
58
+ def get_partial( name )
59
+ @logger.debug( "#{__FILE__}.#{__method__} name=#{name}" )
60
+ return @partials[name] if @partials && @partials[name]
61
+
62
+ partial_file = "#{@@template_path}/#{name}.#{template_extension}"
63
+ @logger.info( "#{__FILE__}.#{__method__} read partial_file=#{partial_file}" )
64
+ File.read( partial_file )
65
+ end
66
+
67
+ # hide @templates - for easier extension
68
+ def get_template( name )
69
+
70
+ @logger.debug( "#{__FILE__}.#{__method__} name=#{name}" )
71
+ return @templates[name] if @templates && @templates[name]
72
+
73
+ template_path = "#{@@template_path}/#{name}.#{@@template_extension}"
74
+ @logger.info( "#{__FILE__}.#{__method__} read template_path=#{template_path}" )
75
+
76
+ File.read( template_path )
77
+ end
78
+
79
+ end # class
80
+
81
+ end # module
data/lib/aws-must.rb ADDED
@@ -0,0 +1,11 @@
1
+ module AwsMust
2
+
3
+ require_relative 'utils/logger'
4
+ require_relative 'utils/hasher'
5
+ require_relative 'aws-must/aws-must'
6
+ require_relative 'aws-must/template'
7
+ require_relative 'aws-must/docu'
8
+
9
+ end
10
+
11
+
@@ -0,0 +1,139 @@
1
+ # -*- mode: ruby -*-
2
+
3
+ # demo.rake
4
+
5
+ # Thank You!
6
+ # http://andyatkinson.com/blog/2014/06/23/sharing-rake-tasks-in-gems
7
+
8
+ require 'json'
9
+
10
+ namespace "demo" do |ns|
11
+
12
+ # --------------------
13
+ # Demo configs
14
+
15
+ cmd = File.join File.dirname(__FILE__), "../../bin/aws-must.rb" # generator being demonstrated
16
+ demo_dir=File.join File.dirname(__FILE__), "../../demo" # directory holding demo configs
17
+ stack="demo" # stack name of the on Amazon
18
+ default_private_key_file="~/.ssh/demo-key/demo-key" # private keyfile corresponding
19
+
20
+ all_regions= ["ap-northeast-1", "ap-southeast-1", "ap-southeast-2", "cn-north-1", "eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-gov-west-1", "us-west-1", "us-west-2"]
21
+
22
+ [
23
+
24
+ { :id => "1", :desc=>"Initial copy", :region=>['eu-central-1'], :ssh => false },
25
+ { :id => "2", :desc=>"Added 'description' -property to YAML file", :region=>['eu-central-1'], :ssh => false },
26
+ { :id => "3", :desc=>"Use resources.mustache -partial to create EC2 intance", :region=>['eu-central-1'], :ssh => false },
27
+ { :id => "4", :desc=>"EC2 instance configuration using YAML-data", :region=>['eu-central-1'], :ssh => false },
28
+ { :id => "5", :desc=>"Add 'Outputs' -section with reference to EC2 instance", :region=>['eu-central-1'], :ssh => false },
29
+ { :id => "6", :desc=>"Add 'Inputs' and 'Mappings' -sections to parametirize", :region=> all_regions, :ssh => false },
30
+ { :id => "7", :desc=>"Added support for input parameters, EC2 tags, Instance security group", :region=>all_regions, :ssh => false },
31
+
32
+
33
+ ].each do |c|
34
+
35
+ desc "Output CF template using configs in '#{demo_dir}/#{c[:id]}' to demonstrate '#{c[:desc]}'"
36
+ task "template-#{c[:id]}" do
37
+ sh "#{cmd} gen -t #{demo_dir}/#{c[:id]} #{demo_dir}/#{c[:id]}/conf.yaml"
38
+ end
39
+
40
+ desc "Output configs in '#{demo_dir}/#{c[:id]}' in JSON to demonstrate '#{c[:desc]}'"
41
+ task "json-#{c[:id]}" do
42
+ sh "#{cmd} json #{demo_dir}/#{c[:id]}/conf.yaml"
43
+ end
44
+
45
+ desc "Check the difference between version '#{c[:id]}' and '#{c[:id].to_i() -1}' "
46
+ task "diff-#{c[:id]}" do
47
+ prev = c[:id].to_i() -1
48
+ # use bash temporary names pipes to diff two stdouts
49
+ # Use `jq` to create canonical pretty print json
50
+ sh "bash -c \"diff <(#{cmd} gen -t #{demo_dir}/#{c[:id]} #{demo_dir}/#{c[:id]}/conf.yaml | jq . ) <(#{cmd} gen -t #{demo_dir}/#{prev} #{demo_dir}/#{prev}/conf.yaml | jq . ) || true \""
51
+ end
52
+
53
+ desc "Create stack #{stack} for demo case #{c[:id]}, supported regions=#{c[:region]}"
54
+ task "stack-create-#{c[:id]}" do
55
+ sh "aws cloudformation create-stack --stack-name #{stack} --template-body \"$(#{cmd} gen -t #{demo_dir}/#{c[:id]} #{demo_dir}/#{c[:id]}/conf.yaml)\""
56
+ end
57
+
58
+ desc "Open html documentation in 'browser' (default 'firefox')"
59
+ task "html-#{c[:id]}", :browser do |t,args|
60
+
61
+ args.with_defaults( browser: "firefox" )
62
+
63
+ sh "#{cmd} doc -t #{demo_dir}/#{c[:id]} | markdown | #{args.browser} \"data:text/html;base64,$(base64 -w 0 <&0)\""
64
+ end
65
+
66
+
67
+ desc "Copy demo case #{c[:id]} to :templateDir and :configDir"
68
+ task "bootstrap-#{c[:id]}", :templateDir,:configDir do |t, args|
69
+
70
+ usage = "Task '#{t}' usage: #{t}[:templateDir,:configDir]"
71
+ raise usage if args.templateDir.nil?
72
+ raise usage if args.configDir.nil?
73
+
74
+ raise "Directory #{args.templateDir} does not exist" unless File.exists?( args.templateDir )
75
+ raise "Directory #{args.configDir} does not exist" unless File.exists?( args.configDir )
76
+
77
+
78
+ puts (<<-EOS) if args.templateDir == args.configDir
79
+
80
+ Warning :templateDir == :configDir == #{args.templateDir}
81
+ Suggestion to keep templates and configurations in a separate directory
82
+
83
+ EOS
84
+
85
+ demoTemplates = File.join demo_dir, c[:id], "*.mustache"
86
+ configFiles = File.join demo_dir, c[:id], "*.yaml"
87
+
88
+ Dir[demoTemplates].each { |f|
89
+ FileUtils.cp(f, args.templateDir, :verbose=>true )
90
+ }
91
+
92
+ Dir[configFiles].each { |f|
93
+ FileUtils.cp(f, args.configDir, :verbose=>true )
94
+ }
95
+
96
+ end
97
+
98
+
99
+ end
100
+
101
+ desc "Show status of CloudFormation stack"
102
+ task "stack-status" do
103
+ sh "aws cloudformation describe-stacks"
104
+ end
105
+
106
+ desc "Ssh connection to ':ipName' using ':private_key' default (#{default_private_key_file})"
107
+ task 'stack-ssh', :ipName, :private_key do |t, args|
108
+
109
+ raise "Task '#{t}' usage: #{t}[ipName,private_key] - exiting" if args.ipName.nil?
110
+
111
+ stack_string = %x{ aws cloudformation describe-stacks --stack-name #{stack} }
112
+ raise "Error #{$?.exitstatus}" if $?.exitstatus != 0
113
+
114
+ args.with_defaults( private_key: default_private_key_file )
115
+
116
+ stack_json = JSON.parse( stack_string )
117
+ ip_json = stack_json["Stacks"][0]['Outputs'].select { |o| o['OutputKey'] == args.ipName }.first
118
+ raise "Could not find ip for 'ipName' '#{args.ipName}' in output section of #{stack} stack" unless ip_json
119
+
120
+ puts ip_json['OutputValue']
121
+ identity="-i #{args.private_key}"
122
+
123
+ sh "ssh ubuntu@#{ip_json['OutputValue']} #{identity}"
124
+ end
125
+
126
+
127
+ desc "Show events of CloudFormation stack '#{stack}'"
128
+ task "stack-events" do
129
+ sh "aws cloudformation describe-stack-events --stack-name #{stack}"
130
+ end
131
+
132
+ desc "Delete stack CloudFormation stack '#{stack}'"
133
+ task "stack-delete" do
134
+ sh "aws cloudformation delete-stack --stack-name #{stack}"
135
+ end
136
+
137
+
138
+ end
139
+
@@ -0,0 +1,83 @@
1
+ # see http://hawkins.io/2013/08/using-the-ruby-logger/
2
+
3
+ # ------------------------------------------------------------------
4
+ # Open up class Hash to add `deep_traverse`
5
+
6
+
7
+ class Hash
8
+ def deep_traverse(&block)
9
+ self.each do |k,v|
10
+ puts k,v
11
+ yield( k,v)
12
+ end
13
+ end
14
+ # stack = self.map{ |k,v| [ [k], v ] }
15
+ # while not stack.empty?
16
+ # key, value = stack.pop
17
+ # yield(key, value)
18
+ # if value.is_a? Hash
19
+ # value.each{ |k,v| stack.push [ key.dup << k, v ] }
20
+ # end
21
+ # end
22
+ # end
23
+
24
+ end
25
+
26
+
27
+
28
+ # ------------------------------------------------------------------
29
+ # implement my own
30
+
31
+ module Utils
32
+
33
+ module Hasher
34
+
35
+ # see http://stackoverflow.com/questions/3748744/traversing-a-hash-recursively-in-ruby
36
+
37
+ def dfs(hsh, &blk)
38
+ return enum_for(:dfs, hsh) unless blk
39
+
40
+ # no change unless a hash
41
+ return hsh unless hsh.is_a? Hash
42
+
43
+ yield hsh
44
+ hsh.each do |k,v|
45
+ if v.is_a? Array
46
+ v.each do |elm|
47
+ dfs(elm, &blk)
48
+ end
49
+ elsif v.is_a? Hash
50
+ dfs(v, &blk)
51
+ end
52
+ end
53
+ return hsh
54
+ # nil
55
+ end
56
+
57
+ # adds comma hash in arrays deeply in obj-hash
58
+ def addComma( obj )
59
+
60
+ if ( obj.is_a?(Hash) )
61
+
62
+ obj2 = dfs(obj) do |o|
63
+ o.each do |k,v|
64
+ if v.is_a? Array
65
+ # add :comma -property expect to the last element
66
+ v.slice(0,v.length-1).each do |elem|
67
+ elem["_comma"] = "," if elem.is_a? Hash
68
+ end
69
+ end
70
+ end
71
+ end # block
72
+
73
+ obj = obj2
74
+
75
+ end
76
+
77
+
78
+ return obj
79
+ end
80
+
81
+ end
82
+
83
+ end # module Utils
@@ -0,0 +1,71 @@
1
+ require 'logger'
2
+
3
+ # see http://hawkins.io/2013/08/using-the-ruby-logger/
4
+
5
+
6
+ module Utils
7
+
8
+ module MyLogger
9
+
10
+ # no logging done
11
+
12
+ class NullLoger < Logger
13
+ def initialize(*args)
14
+ end
15
+
16
+ def add(*args, &block)
17
+ end
18
+ end
19
+
20
+ LOGFILE="apu.tmp"
21
+
22
+ def getLogger( progname, options={} )
23
+
24
+ level = get_level( options )
25
+
26
+ if level.nil?
27
+
28
+ return NullLoger.new
29
+
30
+ else
31
+
32
+ logger = Logger.new( LOGFILE )
33
+ logger.level=level
34
+ logger.progname = progname
35
+ return logger
36
+
37
+ end
38
+
39
+ end # getLogger
40
+
41
+ # ------------------------------------------------------------------
42
+ private
43
+
44
+ def get_level( options )
45
+
46
+ # puts "#{__method__}: options=#{options}"
47
+
48
+ level_name = options && options[:log] ? options[:log] : ENV['LOG_LEVEL']
49
+
50
+ level = case level_name
51
+ when 'warn', 'WARN'
52
+ Logger::WARN
53
+ when 'info', 'INFO'
54
+ Logger::INFO
55
+ when 'debug', 'DEBUG'
56
+ Logger::DEBUG
57
+ when 'error', 'ERROR'
58
+ Logger::ERROR
59
+ else
60
+ nil
61
+ end
62
+
63
+ return level
64
+ end
65
+
66
+
67
+
68
+ end
69
+
70
+
71
+ end