dop_common 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +176 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +177 -0
- data/README.md +48 -0
- data/Rakefile +49 -0
- data/Vagrantfile +25 -0
- data/bin/dop-puppet-autosign +56 -0
- data/doc/examples/example_deploment_plan_v0.0.1.yaml +302 -0
- data/doc/plan_format_v0.0.1.md +919 -0
- data/doc/plan_format_v0.0.2_snippets.md +56 -0
- data/dop_common.gemspec +44 -0
- data/lib/dop_common/affinity_group.rb +57 -0
- data/lib/dop_common/cli/global_options.rb +37 -0
- data/lib/dop_common/cli/log.rb +51 -0
- data/lib/dop_common/cli/node_selection.rb +62 -0
- data/lib/dop_common/command.rb +125 -0
- data/lib/dop_common/config/helper.rb +39 -0
- data/lib/dop_common/config.rb +66 -0
- data/lib/dop_common/configuration.rb +37 -0
- data/lib/dop_common/credential.rb +152 -0
- data/lib/dop_common/data_disk.rb +62 -0
- data/lib/dop_common/dns.rb +55 -0
- data/lib/dop_common/hash_parser.rb +241 -0
- data/lib/dop_common/hooks.rb +81 -0
- data/lib/dop_common/infrastructure.rb +160 -0
- data/lib/dop_common/infrastructure_properties.rb +185 -0
- data/lib/dop_common/interface.rb +113 -0
- data/lib/dop_common/log.rb +78 -0
- data/lib/dop_common/network.rb +85 -0
- data/lib/dop_common/node/config.rb +159 -0
- data/lib/dop_common/node.rb +442 -0
- data/lib/dop_common/node_filter.rb +74 -0
- data/lib/dop_common/plan.rb +188 -0
- data/lib/dop_common/plan_cache.rb +83 -0
- data/lib/dop_common/plan_store.rb +263 -0
- data/lib/dop_common/pre_processor.rb +73 -0
- data/lib/dop_common/run_options.rb +56 -0
- data/lib/dop_common/signal_handler.rb +58 -0
- data/lib/dop_common/state_store.rb +95 -0
- data/lib/dop_common/step.rb +200 -0
- data/lib/dop_common/step_set.rb +41 -0
- data/lib/dop_common/thread_context_logger.rb +77 -0
- data/lib/dop_common/utils.rb +106 -0
- data/lib/dop_common/validator.rb +53 -0
- data/lib/dop_common/version.rb +3 -0
- data/lib/dop_common.rb +32 -0
- data/lib/hiera/backend/dop_backend.rb +94 -0
- data/lib/hiera/dop_logger.rb +20 -0
- data/spec/data/fake_hook_file_invalid +1 -0
- data/spec/data/fake_hook_file_valid +5 -0
- data/spec/data/fake_keyfile +1 -0
- data/spec/dop-puppet-autosign_spec_disable.rb +33 -0
- data/spec/dop_common/affinity_group_spec.rb +41 -0
- data/spec/dop_common/command_spec.rb +83 -0
- data/spec/dop_common/credential_spec.rb +73 -0
- data/spec/dop_common/data_disk_spec.rb +165 -0
- data/spec/dop_common/dns_spec.rb +33 -0
- data/spec/dop_common/hash_parser_spec.rb +181 -0
- data/spec/dop_common/hooks_spec.rb +33 -0
- data/spec/dop_common/infrastructure_properties_spec.rb +224 -0
- data/spec/dop_common/infrastructure_spec.rb +77 -0
- data/spec/dop_common/interface_spec.rb +192 -0
- data/spec/dop_common/network_spec.rb +92 -0
- data/spec/dop_common/node_filter_spec.rb +70 -0
- data/spec/dop_common/node_spec.rb +623 -0
- data/spec/dop_common/plan_cache_spec.rb +46 -0
- data/spec/dop_common/plan_spec.rb +136 -0
- data/spec/dop_common/plan_store_spec.rb +194 -0
- data/spec/dop_common/pre_processor_spec.rb +27 -0
- data/spec/dop_common/run_options_spec.rb +65 -0
- data/spec/dop_common/signal_handler_spec.rb +31 -0
- data/spec/dop_common/step_set_spec.rb +21 -0
- data/spec/dop_common/step_spec.rb +175 -0
- data/spec/dop_common/utils_spec.rb +27 -0
- data/spec/dop_common/validator_spec.rb +47 -0
- data/spec/example_plans_spec.rb +16 -0
- data/spec/fixtures/example_ssh_key +27 -0
- data/spec/fixtures/example_ssh_key.pub +1 -0
- data/spec/fixtures/incl/root_part.yaml +1 -0
- data/spec/fixtures/incl/some_list.yaml +2 -0
- data/spec/fixtures/other_plan_same_nodes.yaml +19 -0
- data/spec/fixtures/simple_include.yaml +6 -0
- data/spec/fixtures/simple_include_with_errors.yaml +4 -0
- data/spec/fixtures/simple_plan.yaml +19 -0
- data/spec/fixtures/simple_plan_invalid.yaml +18 -0
- data/spec/fixtures/simple_plan_modified.yaml +21 -0
- data/spec/spec_helper.rb +106 -0
- 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
|