hosties 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d0ddfeda2e90489df21afb44cb70c5560fd24b12
4
+ data.tar.gz: 7307934a3183c4fcb74d1a4fe3e4c6401ecf7ba0
5
+ SHA512:
6
+ metadata.gz: e8c90640a12836827bc7c0df71a7496389833b5fafcfb495b35355efc650215032ae6e39b8b10870c40b9e36c9f62ed45025347f1d8d051175fe50c04813b7d4
7
+ data.tar.gz: e17ba1f3cb0e4bfa34740edf9491124a97ef467cdd993dd4e478e257cfcf4d556dcdc2e34270962239b05a3114e604a6a8c45d59635bf4e532d55ed743360c1b
@@ -0,0 +1,14 @@
1
+ require 'hosties/definitions'
2
+ require 'hosties/reification'
3
+
4
+ # A library to provide easily readable environment definitions.
5
+ module Hosties
6
+ # Environment definitions, keyed by type
7
+ EnvironmentDefinitions = {}
8
+ # Host definitions, keyed by type
9
+ HostDefinitions = {}
10
+ # Environment instances, definition type => array of instances
11
+ Environments = Hash.new{|h,k| h[k] = []}
12
+ # Maps type => hash of specified 'grouped_by' value to array of matches
13
+ GroupedEnvironments = {}
14
+ end
@@ -0,0 +1,137 @@
1
+ #######################################################################
2
+ # Provide some classes to give us friendly, easy on the eyes syntax #
3
+ # for defining what different types of hosts and environments should #
4
+ # have in order to be valid. #
5
+ #######################################################################
6
+
7
+ # Constrains a named attribute to a provided set of values. This is good
8
+ # for things like describing environments that a set of hosts can be in,
9
+ # for instance Dev, QA, etc
10
+ class AttributeConstraint
11
+ attr_reader :name, :possible_vals
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ @possible_vals = []
16
+ end
17
+
18
+ def can_be(val, *more)
19
+ @possible_vals += (more << val)
20
+ end
21
+ end
22
+
23
+ # Superclass for the host and environment requirement types. This
24
+ # class handles the plumbing of tracking, constraining, and eventually
25
+ # reifying attributes.
26
+ class HasAttributes
27
+ attr_accessor :constraints
28
+ attr_accessor :attributes
29
+ def initialize
30
+ @constraints = {}
31
+ @attributes = []
32
+ end
33
+
34
+ # Specify symbols that will later be reified into attributes
35
+ def have_attributes(attr, *more)
36
+ @attributes += (more << attr)
37
+ end
38
+
39
+ alias_method :have_attribute, :have_attributes
40
+
41
+ # Helpful method to define constraints
42
+ def where(name)
43
+ # Must define the attributes before constraining them
44
+ raise ArgumentError, "Unknown attribute: #{name}" unless @attributes.include? name
45
+ @constraints[name] = AttributeConstraint.new(name)
46
+ end
47
+
48
+ # Check if a given name-value pair is valid given the constraints
49
+ def valid?(name, value)
50
+ if @constraints.include? name then
51
+ constraints[name].possible_vals.include? value
52
+ else true end
53
+ end
54
+ end
55
+
56
+ # Defines what a host of a certain type looks like
57
+ class HostRequirement < HasAttributes
58
+ attr_reader :type, :services
59
+ def initialize(type)
60
+ super()
61
+ @type = type
62
+ @services = []
63
+ end
64
+
65
+ # Services will be provided with a host definition. In order for
66
+ # a host definition to be valid, it must provide service details
67
+ # for all of the services specified by its matching
68
+ # HostRequirement
69
+ def have_services(service, *more)
70
+ @services += (more << service)
71
+ end
72
+
73
+ alias_method :have_service, :have_services
74
+
75
+ def finished
76
+ Hosties::HostDefinitions[@type] = self
77
+ end
78
+ end
79
+
80
+ # Builder method
81
+ def host_type(symbol, &block)
82
+ builder = HostRequirement.new(symbol)
83
+ begin
84
+ builder.instance_eval(&block)
85
+ builder.finished
86
+ rescue ArgumentError => ex
87
+ # TODO: There must be a better way!
88
+ # I'd like to provide some feedback in this case, but I don't
89
+ # like having this show up in test output.
90
+ #puts "Problem defining host \"#{symbol}\": #{ex}"
91
+ end
92
+ end
93
+
94
+ # Used to describe an environment.
95
+ class EnvironmentRequirement < HasAttributes
96
+ attr_reader :type, :hosts, :grouping
97
+ def initialize(type)
98
+ super()
99
+ @type = type
100
+ @hosts = []
101
+ end
102
+
103
+ # Define the hosts that an environment needs to be valid,
104
+ # for instance, maybe you need a :logger host and a
105
+ # :service host at a minimum.
106
+ def need(host1, *more)
107
+ sum = more << host1
108
+ # Array doesn't have an 'exists' method, so behold - map reduce wankery!
109
+ unless sum.map { |x| Hosties::HostDefinitions.include? x }.reduce(true) { |xs, x| x & xs }
110
+ raise ArgumentError, "Unrecognized host type"
111
+ end
112
+ @hosts += sum
113
+ end
114
+
115
+ # Optionally specify an attribute to group by when registering
116
+ # environments of this type.
117
+ def grouped_by(attr)
118
+ raise ArgumentError, "Unknown attribute #{attr}" unless @attributes.include?(attr)
119
+ @grouping = attr
120
+ end
121
+
122
+ def finished
123
+ Hosties::EnvironmentDefinitions[@type] = self
124
+ end
125
+ end
126
+
127
+ # Builder method
128
+ def environment_type(symbol, &block)
129
+ builder = EnvironmentRequirement.new(symbol)
130
+ begin
131
+ builder.instance_eval(&block)
132
+ builder.finished
133
+ rescue ArgumentError => ex
134
+ # TODO: Same as above, find a better way to get this information out
135
+ #puts "Problem describing environment \"#{builder.type}\": #{ex}"
136
+ end
137
+ end
@@ -0,0 +1,127 @@
1
+ # Fancy words, fancy words.
2
+ # Provide some classes to turn a declarative host definition into something
3
+ # more useful in code, applying rules from the definition files to ensure we
4
+ # only get valid stuff.
5
+ class UsesAttributes
6
+ # Oh this old thing...
7
+ def metaclass
8
+ class << self
9
+ self
10
+ end
11
+ end
12
+ def initialize(has_attributes)
13
+ @attributes = has_attributes.attributes
14
+ # Magic.
15
+ has_attributes.attributes.each do |attr|
16
+ # Add in the attribute
17
+ self.metaclass.send(:attr_accessor, attr)
18
+ # Define a constrained setter
19
+ self.metaclass.send(:define_method, attr) do |val|
20
+ raise ArgumentError, "Invalid value" unless has_attributes.valid?(attr, val)
21
+ instance_variable_set "@#{attr}", val
22
+ end
23
+ end
24
+ end
25
+
26
+ # Return a hash after verifying everything was set correctly
27
+ def finish
28
+ retval = {}
29
+ # Ensure all required attributes have been set
30
+ @attributes.each do |attr|
31
+ val = instance_variable_get "@#{attr}"
32
+ raise ArgumentError, "Missing attribute #{attr}" if val.nil?
33
+ retval[attr] = val
34
+ end
35
+ retval
36
+ end
37
+ end
38
+
39
+ class HostBuilder < UsesAttributes
40
+ def initialize(type, hostname)
41
+ if Hosties::HostDefinitions[type].nil? then
42
+ raise ArgumentError, "Unrecognized host type"
43
+ end
44
+ @type = type
45
+ @definition = Hosties::HostDefinitions[@type]
46
+ @hostname = hostname
47
+ @service_ports = {} # Map of service type to port
48
+ super(@definition) # Creates attribute code
49
+ # Services are really just a special kind of attribute, but for now I'll
50
+ # keep them separate. I'm thinking maybe I could add a new type of attribute
51
+ # constraint that let's a user specify that an attribute must be numeric, or
52
+ # a string for instance.
53
+ @definition.services.each do |service_type|
54
+ self.metaclass.send(:attr_accessor, service_type)
55
+ self.metaclass.send(:define_method, service_type) do |port|
56
+ raise ArgumentError, "Port number required" unless port.is_a? Integer
57
+ @service_ports[service_type] = port
58
+ end
59
+ end
60
+ end
61
+
62
+ def finish
63
+ # Ensure all services have been set
64
+ @definition.services.each do |svc|
65
+ raise ArgumentError, "Missing service #{svc}" if @service_ports[svc].nil?
66
+ end
67
+ # TODO: More clever data repackaging
68
+ super.merge({ :hostname => @hostname }).merge(@service_ports)
69
+ end
70
+ end
71
+
72
+ # Turn a description into a useful data structure - and it's validated!
73
+ class EnvironmentBuilder < UsesAttributes
74
+ def initialize(type)
75
+ if Hosties::EnvironmentDefinitions[type].nil? then
76
+ raise ArgumentError, "Unrecognized environment type"
77
+ end
78
+ @hosts = {} # host type => array of hosts' data
79
+ @type = type
80
+ @definition = Hosties::EnvironmentDefinitions[@type]
81
+ super(@definition) # Creates attribute code
82
+ # More magic, this time create a parameterized host builder based
83
+ # on the type of hosts this environment wants. Poor man's currying
84
+ @definition.hosts.each do |host_type|
85
+ self.metaclass.send(:define_method, host_type) do |hostname, &block|
86
+ begin
87
+ builder = HostBuilder.new(host_type, hostname)
88
+ builder.instance_eval(&block)
89
+ if @hosts[host_type].nil? then @hosts[host_type] = [] end
90
+ @hosts[host_type] << builder.finish
91
+ rescue ArgumentError => ex
92
+ #puts "Problem declaring host: #{ex}"
93
+ raise ex
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def finish
100
+ # Verify all of the required hosts were set
101
+ @definition.hosts.each do |host_type|
102
+ raise ArgumentError, "Missing #{host_type} host" unless @hosts.include? host_type
103
+ end
104
+ retval = super.merge({ :hosts => @hosts })
105
+ Hosties::Environments[@type] << retval
106
+ # Add ourselves into the grouped listing if necessary
107
+ attr = @definition.grouping
108
+ unless attr.nil? then # TODO: This is really ugly
109
+ if Hosties::GroupedEnvironments[@type].nil? then
110
+ Hosties::GroupedEnvironments[@type] = Hash.new{|h,k| h[k] = []}
111
+ end
112
+ Hosties::GroupedEnvironments[@type][retval[attr]] << retval
113
+ end
114
+ retval
115
+ end
116
+ end
117
+
118
+ def environment_for(type, &block)
119
+ begin
120
+ builder = EnvironmentBuilder.new(type)
121
+ builder.instance_eval(&block)
122
+ data = builder.finish
123
+ rescue ArgumentError => ex
124
+ puts "Problem declaring environment: #{ex}"
125
+ raise ex
126
+ end
127
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe HasAttributes do
4
+ it 'rejects definitions with constraints on nonexistent attributes' do
5
+ instance = HasAttributes.new
6
+ instance.have_attributes :foo, :bar
7
+ expect { instance.where(:baz).can_be("anything") }.to raise_error(ArgumentError)
8
+ end
9
+ end
10
+
11
+ describe HostRequirement do
12
+ it 'defines host types' do
13
+ # Declare a host type
14
+ host_type :logger do
15
+ have_services :jmx, :rest, :http, :https
16
+ have_attributes :control_mbean, :default_user
17
+ end
18
+ end
19
+ end
20
+
21
+ describe EnvironmentRequirement do
22
+ it 'defines environments with host and attribute requirements' do
23
+ host_type :mutant_maker do end
24
+ host_type :turkey_blaster do end
25
+ environment_type :weird_thanksgiving do
26
+ need :mutant_maker, :turkey_blaster
27
+ have_attribute :weirdness_factor
28
+ end
29
+ end
30
+
31
+ it 'rejects environment definitions that need undefined host types' do
32
+ builder = EnvironmentRequirement.new(:failure)
33
+ expect { builder.need(:nonexistent) }.to raise_error(ArgumentError)
34
+ end
35
+
36
+ it 'rejects groupings for unknown attributes' do
37
+ builder = EnvironmentRequirement.new(:failure)
38
+ expect { builder.grouped_by(:nonexistent) }.to raise_error(ArgumentError)
39
+ end
40
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hosties do
4
+ it 'can declare a host' do
5
+ host_type :special_host do
6
+ end
7
+ instance = HostBuilder.new(:special_host, "0.0.0.0")
8
+ expect(instance.finish).to eq({ :hostname => "0.0.0.0"})
9
+ end
10
+
11
+ it 'catches missing services' do
12
+ host_type :web_host do
13
+ have_service :http
14
+ end
15
+ instance = HostBuilder.new(:web_host, "0.0.0.0")
16
+ expect { instance.finish }.to raise_error(ArgumentError)
17
+ end
18
+
19
+ it 'catches missing attributes' do
20
+ host_type :mud_server do
21
+ have_attribute :version
22
+ end
23
+ instance = HostBuilder.new(:mud_server, "0.0.0.0")
24
+ expect { instance.finish }.to raise_error(ArgumentError)
25
+ end
26
+
27
+ it 'catches non-integral service ports' do
28
+ host_type :web_host do
29
+ have_service :http
30
+ end
31
+ instance = HostBuilder.new(:web_host, "0.0.0.0")
32
+ expect { instance.http 10.4 }.to raise_error(ArgumentError)
33
+ end
34
+
35
+ it 'catches missing host requirements' do
36
+ host_type :type_a do
37
+ end
38
+ host_type :type_b do
39
+ end
40
+ environment_type :needy_environment do
41
+ need :type_a, :type_b
42
+ end
43
+ builder = EnvironmentBuilder.new(:needy_environment)
44
+ builder.type_a "0.0.0.0" do end
45
+ # No type_b specified
46
+ expect { builder.finish }.to raise_error(ArgumentError)
47
+ end
48
+
49
+ it 'can fully declare an environment' do
50
+ # Declare the host types
51
+ host_type :monitoring do
52
+ have_services :logging, :http
53
+ end
54
+ host_type :service_host do
55
+ have_services :service_port, :rest, :jmx
56
+ have_attribute :uuid
57
+ end
58
+ # Declare this product's environment makeup
59
+ environment_type :typical_product do
60
+ need :service_host, :monitoring
61
+ have_attribute :environment
62
+ where(:environment).can_be(:dev, :qa, :live)
63
+ end
64
+ # make one!
65
+ environment_for :typical_product do
66
+ environment :qa
67
+ monitoring "192.168.0.1" do
68
+ logging 8888
69
+ http 80
70
+ end
71
+ monitoring "192.168.0.2" do
72
+ logging 8888
73
+ http 80
74
+ end
75
+ service_host "192.168.0.3" do
76
+ service_port 1234
77
+ rest 8080
78
+ jmx 9876
79
+ uuid "81E3C1D4-C040-4D59-A56F-4273384D576B"
80
+ end
81
+ end
82
+ expect(Hosties::Environments[:typical_product].nil?).to eq(false)
83
+ data = Hosties::Environments[:typical_product].first
84
+ expect(data[:environment]).to eq(:qa)
85
+ expect(data[:hosts][:monitoring].size).to eq(2) # Two monitoring hosts
86
+ expect(data[:hosts][:service_host].size).to eq(1)
87
+ service_host = data[:hosts][:service_host].first
88
+ expect(service_host[:service_port]).to eq(1234)
89
+ expect(service_host[:uuid]).to eq("81E3C1D4-C040-4D59-A56F-4273384D576B")
90
+ end
91
+
92
+ it 'can group environments by attribute' do
93
+ host_type :foo do end
94
+ host_type :bar do end
95
+ environment_type :foobar do
96
+ need :foo, :bar
97
+ have_attribute :env_type
98
+ where(:env_type).can_be :dev, :qa, :live
99
+ grouped_by :env_type
100
+ end
101
+ environment_for :foobar do
102
+ foo "0.0.0.0" do end
103
+ bar "0.0.0.0" do end
104
+ env_type :dev
105
+ end
106
+ environment_for :foobar do
107
+ foo "0.0.0.0" do end
108
+ bar "0.0.0.0" do end
109
+ env_type :qa
110
+ end
111
+ expect(Hosties::GroupedEnvironments[:foobar][:dev].size).to eq(1)
112
+ expect(Hosties::GroupedEnvironments[:foobar][:qa].size).to eq(1)
113
+ end
114
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe UsesAttributes do
4
+ it 'can enforce attribute constraints' do
5
+ definition = HasAttributes.new
6
+ definition.have_attributes(:x)
7
+ definition.where(:x).can_be("hello")
8
+ instance = UsesAttributes.new(definition)
9
+ instance.x "hello"
10
+ expect { instance.x 31 }.to raise_error(ArgumentError)
11
+ end
12
+ it 'catches missing attributes' do
13
+ definition = HasAttributes.new
14
+ definition.have_attributes(:x)
15
+ instance = UsesAttributes.new(definition)
16
+ expect { instance.finish }.to raise_error(ArgumentError)
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+ require 'hosties'
3
+
4
+ RSpec.configure do |config|
5
+ config.color_enabled = true
6
+ config.formatter = 'documentation'
7
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hosties
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ron Dahlgren
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.5'
27
+ description: |2
28
+ Hosties provides an expressive way to define environments, which are in turn
29
+ comprised of lists of roles and the hosts that fill those roles.
30
+ email: ronald.dahlgren@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - lib/hosties/Definitions.rb
36
+ - lib/hosties/Reification.rb
37
+ - lib/hosties.rb
38
+ - spec/definitions_spec.rb
39
+ - spec/hosties_spec.rb
40
+ - spec/reification_spec.rb
41
+ - spec/spec_helper.rb
42
+ homepage: https://github.com/influenza/hosties
43
+ licenses:
44
+ - BSD-new
45
+ metadata: {}
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 2.0.3
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Easy environment description
66
+ test_files:
67
+ - spec/definitions_spec.rb
68
+ - spec/hosties_spec.rb
69
+ - spec/reification_spec.rb
70
+ - spec/spec_helper.rb