dop_common 0.13.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.
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