sortinghat 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.
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: []