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