dop_common 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +176 -0
  6. data/Gemfile +11 -0
  7. data/LICENSE.txt +177 -0
  8. data/README.md +48 -0
  9. data/Rakefile +49 -0
  10. data/Vagrantfile +25 -0
  11. data/bin/dop-puppet-autosign +56 -0
  12. data/doc/examples/example_deploment_plan_v0.0.1.yaml +302 -0
  13. data/doc/plan_format_v0.0.1.md +919 -0
  14. data/doc/plan_format_v0.0.2_snippets.md +56 -0
  15. data/dop_common.gemspec +44 -0
  16. data/lib/dop_common/affinity_group.rb +57 -0
  17. data/lib/dop_common/cli/global_options.rb +37 -0
  18. data/lib/dop_common/cli/log.rb +51 -0
  19. data/lib/dop_common/cli/node_selection.rb +62 -0
  20. data/lib/dop_common/command.rb +125 -0
  21. data/lib/dop_common/config/helper.rb +39 -0
  22. data/lib/dop_common/config.rb +66 -0
  23. data/lib/dop_common/configuration.rb +37 -0
  24. data/lib/dop_common/credential.rb +152 -0
  25. data/lib/dop_common/data_disk.rb +62 -0
  26. data/lib/dop_common/dns.rb +55 -0
  27. data/lib/dop_common/hash_parser.rb +241 -0
  28. data/lib/dop_common/hooks.rb +81 -0
  29. data/lib/dop_common/infrastructure.rb +160 -0
  30. data/lib/dop_common/infrastructure_properties.rb +185 -0
  31. data/lib/dop_common/interface.rb +113 -0
  32. data/lib/dop_common/log.rb +78 -0
  33. data/lib/dop_common/network.rb +85 -0
  34. data/lib/dop_common/node/config.rb +159 -0
  35. data/lib/dop_common/node.rb +442 -0
  36. data/lib/dop_common/node_filter.rb +74 -0
  37. data/lib/dop_common/plan.rb +188 -0
  38. data/lib/dop_common/plan_cache.rb +83 -0
  39. data/lib/dop_common/plan_store.rb +263 -0
  40. data/lib/dop_common/pre_processor.rb +73 -0
  41. data/lib/dop_common/run_options.rb +56 -0
  42. data/lib/dop_common/signal_handler.rb +58 -0
  43. data/lib/dop_common/state_store.rb +95 -0
  44. data/lib/dop_common/step.rb +200 -0
  45. data/lib/dop_common/step_set.rb +41 -0
  46. data/lib/dop_common/thread_context_logger.rb +77 -0
  47. data/lib/dop_common/utils.rb +106 -0
  48. data/lib/dop_common/validator.rb +53 -0
  49. data/lib/dop_common/version.rb +3 -0
  50. data/lib/dop_common.rb +32 -0
  51. data/lib/hiera/backend/dop_backend.rb +94 -0
  52. data/lib/hiera/dop_logger.rb +20 -0
  53. data/spec/data/fake_hook_file_invalid +1 -0
  54. data/spec/data/fake_hook_file_valid +5 -0
  55. data/spec/data/fake_keyfile +1 -0
  56. data/spec/dop-puppet-autosign_spec_disable.rb +33 -0
  57. data/spec/dop_common/affinity_group_spec.rb +41 -0
  58. data/spec/dop_common/command_spec.rb +83 -0
  59. data/spec/dop_common/credential_spec.rb +73 -0
  60. data/spec/dop_common/data_disk_spec.rb +165 -0
  61. data/spec/dop_common/dns_spec.rb +33 -0
  62. data/spec/dop_common/hash_parser_spec.rb +181 -0
  63. data/spec/dop_common/hooks_spec.rb +33 -0
  64. data/spec/dop_common/infrastructure_properties_spec.rb +224 -0
  65. data/spec/dop_common/infrastructure_spec.rb +77 -0
  66. data/spec/dop_common/interface_spec.rb +192 -0
  67. data/spec/dop_common/network_spec.rb +92 -0
  68. data/spec/dop_common/node_filter_spec.rb +70 -0
  69. data/spec/dop_common/node_spec.rb +623 -0
  70. data/spec/dop_common/plan_cache_spec.rb +46 -0
  71. data/spec/dop_common/plan_spec.rb +136 -0
  72. data/spec/dop_common/plan_store_spec.rb +194 -0
  73. data/spec/dop_common/pre_processor_spec.rb +27 -0
  74. data/spec/dop_common/run_options_spec.rb +65 -0
  75. data/spec/dop_common/signal_handler_spec.rb +31 -0
  76. data/spec/dop_common/step_set_spec.rb +21 -0
  77. data/spec/dop_common/step_spec.rb +175 -0
  78. data/spec/dop_common/utils_spec.rb +27 -0
  79. data/spec/dop_common/validator_spec.rb +47 -0
  80. data/spec/example_plans_spec.rb +16 -0
  81. data/spec/fixtures/example_ssh_key +27 -0
  82. data/spec/fixtures/example_ssh_key.pub +1 -0
  83. data/spec/fixtures/incl/root_part.yaml +1 -0
  84. data/spec/fixtures/incl/some_list.yaml +2 -0
  85. data/spec/fixtures/other_plan_same_nodes.yaml +19 -0
  86. data/spec/fixtures/simple_include.yaml +6 -0
  87. data/spec/fixtures/simple_include_with_errors.yaml +4 -0
  88. data/spec/fixtures/simple_plan.yaml +19 -0
  89. data/spec/fixtures/simple_plan_invalid.yaml +18 -0
  90. data/spec/fixtures/simple_plan_modified.yaml +21 -0
  91. data/spec/spec_helper.rb +106 -0
  92. metadata +381 -0
@@ -0,0 +1,188 @@
1
+ #
2
+ #
3
+ #
4
+ require 'yaml'
5
+
6
+ module DopCommon
7
+ class PlanParsingError < StandardError
8
+ end
9
+
10
+ class Plan
11
+ include Validator
12
+ include HashParser
13
+ include RunOptions
14
+
15
+ def initialize(hash)
16
+ @hash = symbolize_keys(hash)
17
+ end
18
+
19
+ def validate
20
+ valitdate_shared_options
21
+ log_validation_method('name_valid?')
22
+ log_validation_method('infrastructures_valid?')
23
+ log_validation_method('nodes_valid?')
24
+ log_validation_method('step_sets_valid?')
25
+ log_validation_method('configuration_valid?')
26
+ log_validation_method('credentials_valid?')
27
+ log_validation_method(:hooks_valid?)
28
+ try_validate_obj("Plan: Can't validate the infrastructures part because of a previous error"){infrastructures}
29
+ try_validate_obj("Plan: Can't validate the nodes part because of a previous error"){nodes}
30
+ try_validate_obj("Plan: Can't validate the steps part because of a previous error"){step_sets}
31
+ try_validate_obj("Plan: Can't validate the credentials part because of a previous error"){credentials}
32
+ try_validate_obj("Infrastructure #{name}: Can't validate hooks part because of a previous error") { hooks }
33
+ end
34
+
35
+ def name
36
+ @name ||= name_valid? ?
37
+ @hash[:name] : Digest::SHA2.hexdigest(@hash.to_s)
38
+ end
39
+
40
+ def infrastructures
41
+ @infrastructures ||= infrastructures_valid? ?
42
+ create_infrastructures : nil
43
+ end
44
+
45
+ def nodes
46
+ @nodes ||= nodes_valid? ?
47
+ inflate_nodes : nil
48
+ end
49
+
50
+ def step_sets
51
+ @step_sets ||= step_sets_valid? ?
52
+ create_step_sets : []
53
+ end
54
+
55
+ def configuration
56
+ @configuration ||= configuration_valid? ?
57
+ DopCommon::Configuration.new(@hash[:configuration]) :
58
+ DopCommon::Configuration.new({})
59
+ end
60
+
61
+ def credentials
62
+ @credentials ||= credentials_valid? ?
63
+ create_credentials : {}
64
+ end
65
+
66
+ def find_node(name)
67
+ nodes.find{|node| node.name == name}
68
+ end
69
+
70
+ def hooks
71
+ @hooks ||= ::DopCommon::Hooks.new(hooks_valid? ? @hash[:hooks] : {})
72
+ end
73
+
74
+ private
75
+
76
+ def name_valid?
77
+ return false if @hash[:name].nil?
78
+ @hash[:name].kind_of?(String) or
79
+ raise PlanParsingError, 'The plan name has to be a String'
80
+ @hash[:name][/^[\w-]+$/,0] or
81
+ raise PlanParsingError, 'The plan name may only contain letters, numbers and underscores'
82
+ end
83
+
84
+ def infrastructures_valid?
85
+ @hash[:infrastructures] or
86
+ raise PlanParsingError, 'Plan: infrastructures hash is missing'
87
+ @hash[:infrastructures].kind_of?(Hash) or
88
+ raise PlanParsingError, 'Plan: infrastructures key has not a hash as value'
89
+ @hash[:infrastructures].any? or
90
+ raise PlanParsingError, 'Plan: infrastructures hash is empty'
91
+ end
92
+
93
+ def create_infrastructures
94
+ @hash[:infrastructures].map do |name, hash|
95
+ ::DopCommon::Infrastructure.new(name, hash, {:parsed_credentials => credentials})
96
+ end
97
+ end
98
+
99
+ def nodes_valid?
100
+ @hash[:nodes] or
101
+ raise PlanParsingError, 'Plan: nodes hash is missing'
102
+ @hash[:nodes].kind_of?(Hash) or
103
+ raise PlanParsingError, 'Plan: nodes key has not a hash as value'
104
+ @hash[:nodes].any? or
105
+ raise PlanParsingError, 'Plan: nodes hash is empty'
106
+ @hash[:nodes].values.all? { |n| n.kind_of?(Hash) } or
107
+ raise PlanParsingError, 'Plan: nodes must be of hash type'
108
+ end
109
+
110
+ def parsed_nodes
111
+ @parsed_nodes ||= @hash[:nodes].map do |name, hash|
112
+ ::DopCommon::Node.new(name.to_s, hash, {
113
+ :parsed_infrastructures => infrastructures,
114
+ :parsed_credentials => credentials,
115
+ :parsed_hooks => hooks,
116
+ :parsed_configuration => configuration,
117
+ })
118
+ end
119
+ end
120
+
121
+ def inflate_nodes
122
+ parsed_nodes.map do |node|
123
+ node.inflatable? ? node.inflate : node
124
+ end.flatten
125
+ end
126
+
127
+ def step_sets_valid?
128
+ case @hash[:steps]
129
+ when nil then return false #steps can be nil for DOPv only plans
130
+ when Array then return true
131
+ when Hash # multiple step_sets defined
132
+ @hash[:steps].any? or
133
+ raise PlanParsingError, 'Plan: the hash in steps must not be empty'
134
+ @hash[:steps].keys.all?{|k| k.kind_of?(String)} or
135
+ raise PlanParsingError, 'Plan: all the keys in the steps hash have to be strings'
136
+ @hash[:steps].values.all?{|v| v.kind_of?(Array)} or
137
+ raise PlanParsingError, 'Plan: all values in the steps hash have to be arrays'
138
+ else
139
+ raise PlanParsingError, 'Plan: steps key has not a array or hash as value'
140
+ end
141
+ true
142
+ end
143
+
144
+ def create_step_sets
145
+ case @hash[:steps]
146
+ when Array
147
+ [::DopCommon::StepSet.new('default', @hash[:steps])]
148
+ when Hash
149
+ @hash[:steps].map do |name, steps|
150
+ ::DopCommon::StepSet.new(name, steps)
151
+ end
152
+ end
153
+ end
154
+
155
+ def configuration_valid?
156
+ return false if @hash[:configuration].nil? # configuration hash is optional
157
+ @hash[:configuration].kind_of? Hash or
158
+ raise PlanParsingError, "Plan: 'configuration' key has not a hash as value"
159
+ end
160
+
161
+ def credentials_valid?
162
+ return false if @hash[:credentials].nil? # credentials hash is optional
163
+ @hash[:credentials].kind_of? Hash or
164
+ raise PlanParsingError, "Plan: 'credentials' key has not a hash as value"
165
+ @hash[:credentials].keys.all?{|k| k.kind_of?(String) or k.kind_of?(Symbol)} or
166
+ raise PlanParsingError, "Plan: all keys in the 'credentials' hash have to be strings or symbols"
167
+ @hash[:credentials].values.all?{|v| v.kind_of?(Hash)} or
168
+ raise PlanParsingError, "Plan: all values in the 'credentials' hash have to be hashes"
169
+ end
170
+
171
+ def hooks_valid?
172
+ return false unless @hash.has_key?(:hooks)
173
+ raise PlanParsingError, "Plan: hooks, if specified, must be a non-empty hash" if
174
+ !@hash[:hooks].kind_of?(Hash) || @hash[:hooks].empty?
175
+ @hash[:hooks].keys.each do |h|
176
+ raise PlanParsingError, "Plan: invalid hook name '#{h}'" unless
177
+ h.to_s =~ /^(pre|post)_(create|update|destroy)_vm$/
178
+ end
179
+ true
180
+ end
181
+
182
+ def create_credentials
183
+ Hash[@hash[:credentials].map do |name, hash|
184
+ [name, ::DopCommon::Credential.new(name, hash)]
185
+ end]
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,83 @@
1
+
2
+ module DopCommon
3
+ class PlanCache
4
+
5
+ def initialize(plan_store)
6
+ @plan_store = plan_store
7
+ @plans = {} # { plan_name => plan }
8
+ @versions = {} # { plan => version }
9
+ @nodes = {} # { node_name => plan }
10
+ end
11
+
12
+ # will return the plan of the node or nil
13
+ # if the node is not in a plan
14
+ def plan_by_node(node_name)
15
+ plan = @nodes[node_name]
16
+ if plan
17
+ refresh_plan(plan)
18
+ # this makes sure the node was not removed
19
+ plan = @nodes[node_name]
20
+ return plan if plan
21
+ end
22
+
23
+ refresh_all
24
+ return @nodes[node_name]
25
+ end
26
+
27
+ private
28
+
29
+ def refresh_plan(plan)
30
+ loaded_version = @versions[plan]
31
+ plan_name = plan.name
32
+ newest_version = @plan_store.show_versions(plan_name).last
33
+ unless loaded_version == newest_version
34
+ remove_plan(plan)
35
+ add_plan(plan_name)
36
+ end
37
+ rescue
38
+ remove_plan(plan)
39
+ end
40
+
41
+ def refresh_all
42
+ loaded_plan_names = @plans.keys
43
+ existing_plan_names = @plan_store.list
44
+ remove_old_plans(loaded_plan_names - existing_plan_names)
45
+ refresh_plans(existing_plan_names)
46
+ end
47
+
48
+ def refresh_plans(plan_names)
49
+ plan_names.each do |plan_name|
50
+ plan = @plans[plan_name]
51
+ if plan
52
+ refresh_plan(plan)
53
+ else
54
+ add_plan(plan_name)
55
+ end
56
+ end
57
+ end
58
+
59
+ def remove_old_plans(plan_names)
60
+ plan_names.each do |plan_name|
61
+ remove_plan(@plans[plan_name])
62
+ end
63
+ end
64
+
65
+ def remove_plan(plan_to_remove)
66
+ @nodes.delete_if{|node_name, plan| plan == plan_to_remove}
67
+ @plans.delete_if{|plan_name, plan| plan == plan_to_remove}
68
+ @versions.delete_if{|plan, version| plan == plan_to_remove}
69
+ end
70
+
71
+ def add_plan(plan_name)
72
+ version = @plan_store.show_versions(plan_name).last
73
+ plan = @plan_store.get_plan(plan_name)
74
+ @plans[plan_name] = plan
75
+ @versions[plan] = version
76
+ plan.nodes.each do |node|
77
+ @nodes[node.name] = plan
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
@@ -0,0 +1,263 @@
1
+ #
2
+ # DOP Common Plan Store
3
+ #
4
+ # This class will store validated and parsed plans and provides easy access to them
5
+ #
6
+ require 'yaml'
7
+ require 'fileutils'
8
+ require 'etc'
9
+ require 'hashdiff'
10
+ require 'lockfile'
11
+
12
+ module DopCommon
13
+ class PlanExistsError < StandardError
14
+ end
15
+
16
+ class PlanStore
17
+
18
+ def initialize(plan_store_dir)
19
+ @plan_store_dir = plan_store_dir
20
+ @lockfiles = {}
21
+
22
+ # make sure the plan directory is created
23
+ FileUtils.mkdir_p(@plan_store_dir) unless File.directory?(@plan_store_dir)
24
+ end
25
+
26
+ # Add a new plan to the plan store
27
+ def add(raw_plan)
28
+ hash, yaml = read_plan_file(raw_plan)
29
+ plan = DopCommon::Plan.new(hash)
30
+
31
+ raise PlanExistsError, "There is already a plan with the name #{plan.name}" if plan_exists?(plan.name)
32
+ raise StandardError, 'Plan not valid. Unable to add' unless plan.valid?
33
+ raise StandardError, 'Some Nodes already exist. Unable to add' if node_duplicates?(plan)
34
+
35
+ versions_dir = File.join(@plan_store_dir, plan.name, 'versions')
36
+ FileUtils.mkdir_p(versions_dir) unless File.directory?(versions_dir)
37
+ run_lock(plan.name) do
38
+ save_plan_yaml(plan.name, yaml)
39
+ end
40
+
41
+ # make sure the state files are present
42
+ dopi_state = File.join(@plan_store_dir, plan.name, 'dopi.yaml')
43
+ dopv_state = File.join(@plan_store_dir, plan.name, 'dopv.yaml')
44
+ FileUtils.touch(dopi_state)
45
+ FileUtils.touch(dopv_state)
46
+
47
+ DopCommon.log.info("New plan #{plan.name} was added")
48
+ plan.name
49
+ end
50
+
51
+ # Update a plan already in the plan store
52
+ def update(raw_plan)
53
+ hash, yaml = read_plan_file(raw_plan)
54
+ plan = DopCommon::Plan.new(hash)
55
+
56
+ raise StandardError, "No plan with the name #{plan.name} found. Unable to update" unless plan_exists?(plan.name)
57
+ raise StandardError, 'Plan not valid. Unable to update' unless plan.valid?
58
+ raise StandardError, 'Some Nodes already exist in other plans. Unable to update' if node_duplicates?(plan)
59
+
60
+ run_lock(plan.name) do
61
+ save_plan_yaml(plan.name, yaml)
62
+ end
63
+ DopCommon.log.info("Plan #{plan.name} was updated")
64
+ plan.name
65
+ end
66
+
67
+ # remove a plan from the plan store
68
+ def remove(plan_name, remove_dopi_state = true, remove_dopv_state = false)
69
+ raise StandardError, "Plan #{plan_name} does not exist" unless plan_exists?(plan_name)
70
+ plan_dir = File.join(@plan_store_dir, plan_name)
71
+ versions_dir = File.join(plan_dir, 'versions')
72
+
73
+ # we have to remove the plan in two steps, so we don't
74
+ # delete the lockfile too soon.
75
+ run_lock(plan_name) do
76
+ FileUtils.remove_entry_secure(versions_dir)
77
+ end
78
+ info_file = File.join(plan_dir, 'run_lock_info')
79
+ FileUtils.remove_entry_secure(info_file)
80
+ if remove_dopi_state
81
+ dopi_state = File.join(plan_dir, 'dopi.yaml')
82
+ FileUtils.remove_entry_secure(dopi_state)
83
+ end
84
+ if remove_dopv_state
85
+ dopv_state = File.join(plan_dir, 'dopv.yaml')
86
+ FileUtils.remove_entry_secure(dopv_state)
87
+ end
88
+ if (Dir.entries(plan_dir) - [ '.', '..' ]).empty?
89
+ FileUtils.remove_entry_secure(plan_dir)
90
+ end
91
+ DopCommon.log.info("Plan #{plan_name} was removed")
92
+ plan_name
93
+ end
94
+
95
+ # return an array with all plan names in the plan store
96
+ def list
97
+ Dir.entries(@plan_store_dir).select do |entry|
98
+ versions_dir = File.join(@plan_store_dir, entry, "versions")
99
+ add_entry = true
100
+ add_entry = false if ['.', '..'].include?(entry)
101
+ add_entry = false if Dir[versions_dir + '/*.yaml'].empty?
102
+ add_entry
103
+ end
104
+ end
105
+
106
+ # returns a sorted array of versions for a plan (oldest version first)
107
+ def show_versions(plan_name)
108
+ raise StandardError, "Plan #{plan_name} does not exist" unless plan_exists?(plan_name)
109
+ versions_dir = File.join(@plan_store_dir, plan_name, 'versions')
110
+ Dir[versions_dir + '/*.yaml'].map {|yaml_file| File.basename(yaml_file, '.yaml')}.sort
111
+ end
112
+
113
+ # returns the yaml file content for the specified plan and version
114
+ # Returns the latest version if no version is specified
115
+ def get_plan_yaml(plan_name, version = :latest)
116
+ raise StandardError, "Plan #{plan_name} does not exist" unless plan_exists?(plan_name)
117
+
118
+ versions = show_versions(plan_name)
119
+ version = versions.last if version == :latest
120
+ raise StandardError, "Version #{version} of plan #{plan_name} not found" unless versions.include?(version)
121
+
122
+ yaml_file = File.join(@plan_store_dir, plan_name, 'versions', version + '.yaml')
123
+ File.read(yaml_file)
124
+ end
125
+
126
+ # return the hash for the plan in the store for a specific
127
+ # version. Returns the latest version if no version is specified
128
+ def get_plan_hash(plan_name, version = :latest)
129
+ yaml = get_plan_yaml(plan_name, version)
130
+ YAML.load(yaml)
131
+ end
132
+
133
+ # Get the plan object for the specified version directly
134
+ def get_plan(plan_name, version = :latest)
135
+ hash = get_plan_hash(plan_name, version)
136
+ DopCommon::Plan.new(hash)
137
+ end
138
+
139
+ def get_plan_hash_diff(plan_name, old_version, new_version = :latest)
140
+ old_hash = get_plan_hash(plan_name, old_version)
141
+ new_hash = get_plan_hash(plan_name, new_version)
142
+ HashDiff.best_diff(old_hash, new_hash)
143
+ end
144
+
145
+ # A run lock is used in all operations which change plans in the plan store.
146
+ def run_lock(plan_name)
147
+ remove_stale_lock(plan_name)
148
+ lockfile = run_lockfile(plan_name)
149
+ lockfile.lock
150
+ write_run_lock_info(plan_name, lockfile)
151
+ yield
152
+ rescue Lockfile::TimeoutLockError
153
+ raise StandardError, read_run_lock_info(plan_name)
154
+ ensure
155
+ lockfile.unlock if run_lock?(plan_name)
156
+ end
157
+
158
+ # return true if we have a run lock
159
+ def run_lock?(plan_name)
160
+ run_lockfile(plan_name).locked?
161
+ end
162
+
163
+ def state_store(plan_name, app_name)
164
+ state_file = File.join(@plan_store_dir, plan_name, app_name + '.yaml')
165
+ DopCommon::StateStore.new(state_file, plan_name, self)
166
+ end
167
+
168
+ # Returns true if a plan with that name already exists
169
+ # in the plan store.
170
+ def plan_exists?(plan_name)
171
+ versions_dir = File.join(@plan_store_dir, plan_name, 'versions')
172
+ Dir[versions_dir + '/*.yaml'].any?
173
+ end
174
+
175
+ # returns an array with [hash, yaml] of the plan. The plans should always be
176
+ # loaded with this method to make sure the plan is parsed with the
177
+ # pre_processor
178
+ def read_plan_file(raw_plan)
179
+ if raw_plan.kind_of?(Hash)
180
+ [raw_plan, raw_plan.to_yaml]
181
+ else
182
+ parsed_plan = PreProcessor.load_plan(raw_plan)
183
+ [YAML.load(parsed_plan), parsed_plan]
184
+ end
185
+ end
186
+
187
+ private
188
+
189
+ # returns true if a node in the plan is already present
190
+ # in an other plan already in the store.
191
+ def node_duplicates?(plan)
192
+ other_plans = list - [ plan.name ]
193
+ nodes = plan.nodes.map{|n| n.name}
194
+ other_plans.any? do |other_plan_name|
195
+ other_plan = get_plan(other_plan_name)
196
+ other_nodes = other_plan.nodes.map{|n| n.name}
197
+ duplicates = nodes & other_nodes
198
+ unless duplicates.empty?
199
+ DopCommon.log.error("Node(s) #{duplicates.join(', ')} already exist in plan #{other_plan_name}")
200
+ return true
201
+ end
202
+ end
203
+ false
204
+ end
205
+
206
+ # save a new version of the plan to the store
207
+ def save_plan_yaml(plan_name, yaml)
208
+ file_name = File.join(@plan_store_dir, plan_name, 'versions', new_version_string + '.yaml')
209
+ file = File.new(file_name, 'w')
210
+ file.write(yaml)
211
+ file.close
212
+ end
213
+
214
+ def new_version_string
215
+ time = Time.now.utc
216
+ usec = time.usec.to_s.rjust(6, '0')
217
+ time.strftime("%Y%m%d%H%M%S#{usec}")
218
+ end
219
+
220
+ def run_lockfile(plan_name)
221
+ lockfile = File.join(@plan_store_dir, plan_name, 'run_lock')
222
+ options = {:retry => 0, :timeout => 1, :max_age => 60}
223
+ @lockfiles[plan_name] ||= Lockfile.new(lockfile, options)
224
+ end
225
+
226
+ def stale_lock?(plan_name)
227
+ runlock_info = YAML.load(read_run_lock_info(plan_name))
228
+ pid = runlock_info['PID'].to_i
229
+ begin
230
+ Process.getpgid(pid)
231
+ false
232
+ rescue Errno::ESRCH
233
+ true
234
+ end
235
+ end
236
+
237
+ def remove_stale_lock(plan_name)
238
+ lockfile = run_lockfile(plan_name)
239
+ if File.exists?(lockfile.path) and stale_lock?(plan_name)
240
+ DopCommon.log.warn("Removing stale lockfile '#{lockfile.path}'")
241
+ File.delete(lockfile.path)
242
+ end
243
+ end
244
+
245
+ def write_run_lock_info(plan_name, lockfile)
246
+ info_file = File.join(@plan_store_dir, plan_name, 'run_lock_info')
247
+ user = Etc.getpwuid(Process.uid)
248
+ File.open(info_file, 'w') do |f|
249
+ f.puts "# A run lock for the plan #{plan_name} is in place!"
250
+ f.puts "Time: #{Time.now}"
251
+ f.puts "User: #{user.name}"
252
+ f.puts "PID: #{Process.pid}"
253
+ f.puts "Lockfile: #{lockfile.path}"
254
+ end
255
+ end
256
+
257
+ def read_run_lock_info(plan_name)
258
+ info_file = File.join(@plan_store_dir, plan_name, 'run_lock_info')
259
+ File.read(info_file)
260
+ end
261
+
262
+ end
263
+ end
@@ -0,0 +1,73 @@
1
+ #
2
+ # This is the plan preprocessor which merges the individual files
3
+ # together.
4
+ #
5
+ require 'yaml'
6
+ require 'pathname'
7
+
8
+ module DopCommon
9
+ class PreProcessor
10
+
11
+ REGEXP = /(?:^| )include: (\S+)/
12
+
13
+ def self.load_plan(file)
14
+ file_abs = Pathname.new(file).expand_path.to_s
15
+ parse_file(file_abs, []).join
16
+ end
17
+
18
+ private
19
+
20
+ def self.parse_file(file, file_stack)
21
+ detect_loop(file_stack, file)
22
+ validate_file(file)
23
+ content = []
24
+ File.readlines(file).each_with_index do |line, i|
25
+ new_file_stack = file_stack.dup
26
+ new_file_stack << [file, i]
27
+ position = (line =~ REGEXP)
28
+ if position
29
+ position += 1 if position > 0
30
+ filtered_name = filter_name(line[REGEXP, 1])
31
+ new_file = absolute_filepath(file, filtered_name)
32
+ spacer = ' ' * position
33
+ content += parse_file(new_file, new_file_stack).map {|l| spacer + l}
34
+ else
35
+ content << line
36
+ end
37
+ end
38
+ content
39
+ end
40
+
41
+ def self.absolute_filepath(file, new_file)
42
+ if Pathname.new(new_file).absolute?
43
+ new_file
44
+ else
45
+ base_dir = Pathname.new(file).expand_path.dirname
46
+ File.join(base_dir, new_file)
47
+ end
48
+ end
49
+
50
+ def self.filter_name(file)
51
+ file[/^"(.*)"$/, 1] or
52
+ file[/^'(.*)'$/, 1] or
53
+ file
54
+ end
55
+
56
+ def self.validate_file(file)
57
+ File.exist?(file) or
58
+ raise PlanParsingError, "PreProcessor: The included file #{file} does not exist!"
59
+ File.readable?(file) or
60
+ raise PlanParsingError, "PreProcessor: The included file #{file} is not readable!"
61
+ end
62
+
63
+ def self.detect_loop(file_stack, file)
64
+ files = file_stack.map{|f| f[0] }
65
+ if files.include?(file)
66
+ files_i = file_stack.map{|f| "#{f[0]}:#{f[1]}" }
67
+ files_i << file
68
+ raise PlanParsingError, "PreProcessor: Include loop detected [ #{files_i.join(' --> ')} ]"
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,56 @@
1
+ #
2
+ # Parsing of values that can be set on multiple levels
3
+ #
4
+ require 'yaml'
5
+
6
+ module DopCommon
7
+ module RunOptions
8
+ include Validator
9
+
10
+ def valitdate_shared_options
11
+ log_validation_method('max_in_flight_valid?')
12
+ log_validation_method('max_per_role_valid?')
13
+ log_validation_method('canary_host_valid?')
14
+ end
15
+
16
+ def max_in_flight
17
+ @max_in_flight ||= max_in_flight_valid? ?
18
+ @hash[:max_in_flight] : nil
19
+ end
20
+
21
+ def max_per_role
22
+ @max_per_role ||= max_per_role_valid? ?
23
+ @hash[:max_per_role] : nil
24
+ end
25
+
26
+ def canary_host
27
+ @canary_host ||= canary_host_valid? ?
28
+ @hash[:canary_host] : false
29
+ end
30
+
31
+ private
32
+
33
+ def max_in_flight_valid?
34
+ return false if @hash[:max_in_flight].nil? # max_in_flight is optional
35
+ @hash[:max_in_flight].kind_of?(Fixnum) or
36
+ raise PlanParsingError, 'Plan: max_in_flight has to be a number'
37
+ @hash[:max_in_flight] >= -1 or
38
+ raise PlanParsingError, 'Plan: max_in_flight has to be greater than -1'
39
+ end
40
+
41
+ def max_per_role_valid?
42
+ return false if @hash[:max_per_role].nil? # max_per_role is optional
43
+ @hash[:max_per_role].kind_of?(Fixnum) or
44
+ raise PlanParsingError, 'Plan: max_per_role has to be a number'
45
+ @hash[:max_per_role] >= -1 or
46
+ raise PlanParsingError, 'Plan: max_per_role has to be greater than -1'
47
+ end
48
+
49
+ def canary_host_valid?
50
+ return false if @hash[:canary_host].nil?
51
+ @hash[:canary_host].kind_of?(TrueClass) or @hash[:canary_host].kind_of?(FalseClass) or
52
+ raise PlanParsingError, "Step #{@name}: The value for canary_host must be boolean"
53
+ end
54
+
55
+ end
56
+ end