hosties 1.0.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
+ 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