sortinghat 0.1.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: bf767b88b4dc1e13e2c2ba670b706efc8ce09d33
4
+ data.tar.gz: 42cda4780b90f711e220d44f7f46baa96d93d402
5
+ SHA512:
6
+ metadata.gz: cde94d2d42010301ed9131a059086021067279fdd046069468f3ee9b3ec622ecdf43619a1a69360e36323963d15a4d8ded25fbaf1c19437f502b93c66ce8058d
7
+ data.tar.gz: 2845e1f467a07980e370e465e16f38fffc385cc02dd6eddd966465e0ed359f497dd9d9f7f700c0d3efb4213eabb99e14b291fff5302d97f51bba17c200fc50d2
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.2.3
5
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sortinghat.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 D. Pramann
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.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Sortinghat
2
+
3
+ Sortinghat is a unqiue Ruby gem that allows AWS AutoScaling instances to name themselves.
4
+
5
+ We all understand that naming your cattle is bad, they shouldn't be pets.. but hostnames are handy and readable, and [insert reason].
6
+
7
+ When the Sorting Hat is given specific arguments, it can find the gaps in current prefixes or +1 from the last current prefix and name the instance accordingly; along with updating Route53.
8
+
9
+ It follows a specific pattern for hostnames/fqdn:
10
+
11
+ ```
12
+ [client]-[environment]-[type][prefix].[domain].com.
13
+ ```
14
+ For example:
15
+
16
+ ```
17
+ nike-prod-nginx09.prod-internal.nike.com
18
+ ```
19
+
20
+ ## Installation:
21
+
22
+ Install however you please to your AMI(s) with:
23
+
24
+ $ gem install sortinghat
25
+
26
+ ## Requirements:
27
+
28
+ The gem itself was developed under Ruby 2.0.0 to work with CentOS 7.
29
+
30
+ It requires the following gems:
31
+ * aws-sdk ~> 2
32
+ * pure_json
33
+
34
+ During actually usage, the gem requires that the instance have the following IAM actions allowed via policy:
35
+ * autoscaling:Describe*
36
+ * ec2:DescribeInstances
37
+ * ec2:CreateTags
38
+ * route53:ListHostedZones
39
+ * route53:ChangeResourceRecordSets
40
+
41
+ ## Usage:
42
+
43
+ Note: The Sorting Hat requires root privileges to write to files under /etc/.
44
+
45
+ Have cloud-init, cfn-init, or [x/y/z], issue the following command:
46
+
47
+ $ sortinghat -c [client] -e [environment] -t [type] -r [region] -z [domain]
48
+
49
+ Note: [domain] should be in the format of [domain].com, just like the AWS Console reports for the HostedZone. No need to add the trailing dot, it will be added should you forget.
50
+
51
+ The Sorting Hat will log to syslog for information.
52
+
53
+ The Sorting Hat may be re-run, provided you remove the empty file located at '/etc/.sorted'.
54
+
55
+ ## Development
56
+
57
+ Need to develop on an EC2 instance with metadata available or spoof it somehow.
58
+
59
+ Clone:
60
+
61
+ $ git clone https://github.com/praymann/sortinghat
62
+
63
+ Execute:
64
+
65
+ $ bundle install
66
+
67
+ Run:
68
+
69
+ $ bundle exec bin/sortinghat -h
70
+
71
+
72
+ ## Contributing
73
+
74
+ Bug reports and pull requests are welcome on GitHub at https://github.com/praymann/sortinghat.
75
+
76
+
77
+ ## License
78
+
79
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
80
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/sortinghat ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sortinghat'
4
+ require 'optparse'
5
+
6
+ options = {}
7
+
8
+ parser = OptionParser.new do |opt|
9
+ opt.banner = "Usage: sortinghat [OPTIONS]"
10
+ opt.separator ""
11
+ opt.separator "All options are required for the Sorting Hat to function."
12
+ opt.separator "A trailing dot will be added to the zone should you forget it."
13
+ opt.separator ""
14
+ opt.separator "Options:"
15
+
16
+ opt.on("-c", "--client CLIENT", "Client name") do |client|
17
+ options[:client] = client
18
+ end
19
+
20
+ opt.on("-e", "--env ENVIRONMENT", "Environment name") do |environment|
21
+ options[:env] = environment
22
+ end
23
+
24
+ opt.on("-t", "--type TYPE", "Type name") do |type|
25
+ options[:type] = type
26
+ end
27
+
28
+ opt.on("-r", "--region REGION", "Region name") do |region|
29
+ options[:region] = region
30
+ end
31
+
32
+ opt.on("-z", "--zone DOMAIN", "Hosted Zone") do |zone|
33
+ options[:zone] = zone
34
+ end
35
+
36
+ opt.on("-h", "--help", "Help menu") do
37
+ puts parser
38
+ exit 0
39
+ end
40
+ end
41
+
42
+ parser.parse!
43
+
44
+ if options.length < 5
45
+ abort "All options are required."
46
+ end
47
+
48
+ this = Sortinghat::Banquet.new(options)
49
+
50
+ this.dejavu?
51
+
52
+ this.start!
data/lib/sortinghat.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "sortinghat/version"
2
+
3
+ require "sortinghat/banquet"
4
+
5
+ require "sortinghat/aws"
@@ -0,0 +1,137 @@
1
+ require 'aws-sdk'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'date'
5
+
6
+ module Sortinghat
7
+ class AWS
8
+ def initialize( region = 'us-east-1' )
9
+ # Be using this lots, make it an instance variable
10
+ @region = region
11
+
12
+ # Set a generic client for use
13
+ @client = Aws::EC2::Client.new(region: @region)
14
+
15
+ # Create a syslog for us to use as an instance variable
16
+ @log = Syslog::Logger.new 'sortinghat'
17
+ end
18
+
19
+ # Method to discover all the alive auto-scaling instances
20
+ # Returns array of the Name tag values of the instances
21
+ def discover()
22
+ # Temporay array for use
23
+ ids = Array.new
24
+
25
+ # Start a new client
26
+ autoscale = Aws::AutoScaling::Client.new( region: @region )
27
+
28
+ # Use the client to grab all autoscaling instances
29
+ resp = autoscale.describe_auto_scaling_instances()
30
+ @log.info("Grabbed all AutoScaling instances via aws-sdk")
31
+
32
+ # Grab their instanceId(s)
33
+ resp.auto_scaling_instances.each do |instance|
34
+ ids << idtoname(instance.instance_id)
35
+ end
36
+
37
+ # Return the ids
38
+ ids
39
+ end
40
+
41
+ # Method to set the Name tag on the current instance
42
+ # Returns nothing
43
+ def settag!(hostname)
44
+ # Use the instance varible client to create a new Resource
45
+ resource = Aws::EC2::Resource.new(client: @client)
46
+
47
+ # Use the resource, to find current instance, and set the Name tag
48
+ resource.instance(grabinstanceid()).create_tags({
49
+ tags: [
50
+ {
51
+ key: 'Name',
52
+ value: hostname,
53
+ },
54
+ ]
55
+ })
56
+ @log.info("Set Name tag to #{hostname} via aws-sdk")
57
+ end
58
+
59
+ # Method to remove the Name tag, and set a temporary one
60
+ # Returns nothing
61
+ def removetag!()
62
+ # Use the instance varible client to create a new Resource
63
+ resource = Aws::EC2::Resource.new(client: @client)
64
+
65
+ # Use the resource, to find current instance, and set the Name tag
66
+ resource.instance(grabinstanceid()).create_tags({
67
+ tags: [
68
+ {
69
+ key: 'Name',
70
+ value: "sortinghat-#{rand(100)}",
71
+ },
72
+ ]
73
+ })
74
+ @log.info("Set Name tag to temporary #{hostname} via aws-sdk")
75
+ end
76
+
77
+ # Method to set the A record in Route53
78
+ # Returns nothing
79
+ def setroute53(zone, fqdn)
80
+ # Create a new client, and use it to update/insert our A record
81
+ Aws::Route53::Client.new(region: @region).change_resource_record_sets({
82
+ hosted_zone_id: zonetoid(zone),
83
+ change_batch: {
84
+ comment: "Sorting Hat #{Date.today.to_s}",
85
+ changes: [
86
+ {
87
+ action: 'UPSERT',
88
+ resource_record_set: {
89
+ name: fqdn,
90
+ type: 'A',
91
+ ttl: '30',
92
+ resource_records: [
93
+ {
94
+ value: grabinstanceprivateip()
95
+ },
96
+ ],
97
+ },
98
+ },
99
+ ],
100
+ },
101
+ })
102
+ @log.info("Issued UPSERT to Route53 for #{fqdn}")
103
+ end
104
+
105
+ def privateip()
106
+ return grabinstanceprivateip()
107
+ end
108
+
109
+ private
110
+
111
+ def idtoname(instance_id)
112
+ resource = Aws::EC2::Resource.new(client: @client)
113
+ resource.instance(instance_id).tags.each do |tag|
114
+ if tag.key == 'Name'
115
+ return tag.value
116
+ end
117
+ end
118
+ end
119
+
120
+ def zonetoid(hostedzone)
121
+ resp = Aws::Route53::Client.new(region: @region).list_hosted_zones()
122
+ resp.hosted_zones.each do |zone|
123
+ if zone.name == hostedzone
124
+ return zone.id
125
+ end
126
+ end
127
+ end
128
+
129
+ def grabinstanceid()
130
+ return Net::HTTP.get_response(URI.parse("http://169.254.169.254/latest/meta-data/instance-id")).body
131
+ end
132
+
133
+ def grabinstanceprivateip()
134
+ return Net::HTTP.get_response(URI.parse("http://169.254.169.254/latest/meta-data/local-ipv4")).body
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,154 @@
1
+ require 'syslog/logger'
2
+ require 'fileutils'
3
+
4
+ module Sortinghat
5
+ class Banquet
6
+
7
+ # Creation method
8
+ def initialize(options = {})
9
+ # Check that we have write premissions
10
+ checkpermissions()
11
+
12
+ # Create a syslog for us to use as an instance variable
13
+ @log = Syslog::Logger.new 'sortinghat'
14
+
15
+ # Append a trailing dot to the zone, if there isn't one
16
+ if options[:zone][-1, 1] != '.'
17
+ options[:zone] << '.'
18
+ end
19
+
20
+ # Save the options as instance variable
21
+ @options = options
22
+
23
+ # Create an instance varible to contain our AWS calls
24
+ @aws = Sortinghat::AWS.new(@options[:region])
25
+ end
26
+
27
+ # Method to figure out if we've been here before
28
+ def dejavu?
29
+ # Check for sentinel file
30
+ if File.exists?('/etc/.sorted')
31
+ # We found it, log error and exit successfully
32
+ @log.error('Found /etc/.sorted, refusing to sort.')
33
+ abort('Found /etc/.sorted, refusing to sort.')
34
+ end
35
+ end
36
+
37
+ # Main method of Sortinghat
38
+ def start!
39
+ # Find out who is who, instances alive
40
+ alive = cleanup(@aws.discover())
41
+
42
+ # Given the alive instances, find our prefix
43
+ @prefix = ensurezero(selection(alive))
44
+
45
+ # Put together hostname/fqdn
46
+ construction()
47
+
48
+ @aws.settag!(@hostname)
49
+
50
+ # Find out who is who, instances alive
51
+ alive = cleanup(@aws.discover())
52
+
53
+ unless alive.uniq.length == alive.length
54
+ # There are duplicates, remove tag, wait, restart
55
+ @aws.removetag!()
56
+ sleep rand(10)
57
+ start!()
58
+ end
59
+
60
+ # Register in DNS
61
+ @aws.setroute53(@options[:zone], @fqdn)
62
+
63
+ # Set the localhost hostname
64
+ setlocal()
65
+
66
+ # Set /etc/hosts
67
+ sethostsfile()
68
+
69
+ # Throw the hostname in /etc/sysconfig/httpd (if exists)
70
+ givetohttpd()
71
+
72
+ # All done
73
+ finish!()
74
+ end
75
+
76
+ # Last method of Sortinghat
77
+ def finish!
78
+ # Create our sentinel file
79
+ FileUtils.touch('/etc/.sorted')
80
+ end
81
+
82
+ private
83
+
84
+ def checkpermissions()
85
+ unless File.stat('/etc/hosts').writable?
86
+ # We found it, log error and exit successfully
87
+ @log.error('Can not write to /etc, missing required permissions.')
88
+ abort('Can not write to /etc, are you root?')
89
+ end
90
+ end
91
+
92
+ def cleanup(array)
93
+ array.select! { |name| name.include?(@options[:env]) and name.include?(@options[:client]) and name.include?(@options[:type]) }
94
+ end
95
+
96
+ def selection(array)
97
+ # Array to store the numbers already taken
98
+ taken = Array.new
99
+
100
+ # Filter the incoming array, find the numbers and store them in the taken Array
101
+ array.each { |string| taken << string.scan(/\d./).join('').sub(/^0+/, '').to_i }
102
+
103
+ # We have two digits, define our range of numbers
104
+ limits = (1..99).to_a
105
+
106
+ # Return the first value once we find what isn't taken in our range of numbers
107
+ (limits - taken)[0]
108
+ end
109
+
110
+ def ensurezero(prefix)
111
+ if prefix < 10
112
+ prefix.to_s.rjust(2, "0")
113
+ end
114
+ end
115
+
116
+ def construction()
117
+ @hostname = "#{@options[:client]}-#{@options[:env]}-#{@options[:type]}#{@prefix.to_s}-#{@options[:region]}"
118
+ @fqdn = "#{@options[:client]}-#{@options[:env]}-#{@options[:type]}#{@prefix.to_s}-#{@options[:region]}.#{@options[:zone]}"
119
+ end
120
+
121
+ def setlocal()
122
+ if system("hostnamectl set-hostname #{@fqdn}")
123
+ @log.info("Set the localhost hostname to #{@fqdn}.")
124
+ end
125
+ end
126
+
127
+ def sethostsfile()
128
+ # Store the ip address so we only make one metadata call here
129
+ privateip = @aws.privateip()
130
+ if File.readlines('/etc/hosts').grep(/#{@hostname}|#{privateip}/).size < 1
131
+ File.open('/etc/hosts', 'a') do |file|
132
+ file.puts "#{@privateip} \t #{@hostname} #{@fqdn}"
133
+ end
134
+ @log.info("Added hostname(s) to /etc/hosts.")
135
+ else
136
+ @log.warn("The hostname(s) were already in /etc/hosts.")
137
+ end
138
+ end
139
+
140
+ def givetohttpd()
141
+ file = '/etc/sysconfig/httpd'
142
+ if File.exists?(file)
143
+ if File.readlines(file).grep(/HOSTNAME/).size < 1
144
+ @log.info("Found #{file}, appending HOSTNAME=#{@hostname}.")
145
+ File.open(file, 'a') do |file|
146
+ file.puts "HOSTNAME=#{@hostname}"
147
+ end
148
+ else
149
+ @log.warn("Found HOSTNAME already in #{file}")
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,3 @@
1
+ module Sortinghat
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sortinghat/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sortinghat"
8
+ spec.version = Sortinghat::VERSION
9
+ spec.authors = ["D. Pramann"]
10
+ spec.email = ["daniel@pramann.org"]
11
+
12
+ spec.summary = %q{Have auto-scaling instances name themselves!}
13
+ spec.description = %q{Ruby gem which when given arguements, allows an instance in an auto-scaling group to name/dns/tag itself.}
14
+ spec.homepage = "https://github.com/praymann/sortinghat"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "bin"
27
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.10"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec"
33
+
34
+ spec.add_runtime_dependency "aws-sdk", '~> 2'
35
+ spec.add_runtime_dependency "json_pure"
36
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sortinghat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - D. Pramann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-17 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.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
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
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: aws-sdk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json_pure
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Ruby gem which when given arguements, allows an instance in an auto-scaling
84
+ group to name/dns/tag itself.
85
+ email:
86
+ - daniel@pramann.org
87
+ executables:
88
+ - sortinghat
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".travis.yml"
95
+ - Gemfile
96
+ - LICENSE.txt
97
+ - README.md
98
+ - Rakefile
99
+ - bin/sortinghat
100
+ - lib/sortinghat.rb
101
+ - lib/sortinghat/aws.rb
102
+ - lib/sortinghat/banquet.rb
103
+ - lib/sortinghat/version.rb
104
+ - sortinghat.gemspec
105
+ homepage: https://github.com/praymann/sortinghat
106
+ licenses:
107
+ - MIT
108
+ metadata:
109
+ allowed_push_host: https://rubygems.org
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.4.8
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Have auto-scaling instances name themselves!
130
+ test_files: []