cult 0.1.1.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +240 -0
  6. data/Rakefile +6 -0
  7. data/cult +1 -0
  8. data/cult.gemspec +38 -0
  9. data/doc/welcome.txt +1 -0
  10. data/exe/cult +86 -0
  11. data/lib/cult/artifact.rb +45 -0
  12. data/lib/cult/cli/common.rb +265 -0
  13. data/lib/cult/cli/console_cmd.rb +124 -0
  14. data/lib/cult/cli/cri_extensions.rb +84 -0
  15. data/lib/cult/cli/init_cmd.rb +116 -0
  16. data/lib/cult/cli/load.rb +26 -0
  17. data/lib/cult/cli/node_cmd.rb +205 -0
  18. data/lib/cult/cli/provider_cmd.rb +123 -0
  19. data/lib/cult/cli/role_cmd.rb +149 -0
  20. data/lib/cult/cli/task_cmd.rb +140 -0
  21. data/lib/cult/commander.rb +103 -0
  22. data/lib/cult/config.rb +22 -0
  23. data/lib/cult/definition.rb +112 -0
  24. data/lib/cult/driver.rb +88 -0
  25. data/lib/cult/drivers/common.rb +192 -0
  26. data/lib/cult/drivers/digital_ocean_driver.rb +179 -0
  27. data/lib/cult/drivers/linode_driver.rb +282 -0
  28. data/lib/cult/drivers/load.rb +26 -0
  29. data/lib/cult/drivers/script_driver.rb +27 -0
  30. data/lib/cult/drivers/vultr_driver.rb +217 -0
  31. data/lib/cult/named_array.rb +129 -0
  32. data/lib/cult/node.rb +62 -0
  33. data/lib/cult/project.rb +169 -0
  34. data/lib/cult/provider.rb +134 -0
  35. data/lib/cult/role.rb +213 -0
  36. data/lib/cult/skel.rb +85 -0
  37. data/lib/cult/task.rb +64 -0
  38. data/lib/cult/template.rb +92 -0
  39. data/lib/cult/transferable.rb +61 -0
  40. data/lib/cult/version.rb +3 -0
  41. data/lib/cult.rb +4 -0
  42. data/skel/.cultconsolerc +4 -0
  43. data/skel/.cultrc.erb +29 -0
  44. data/skel/README.md.erb +22 -0
  45. data/skel/keys/.keep +0 -0
  46. data/skel/nodes/.keep +0 -0
  47. data/skel/providers/.keep +0 -0
  48. data/skel/roles/all/role.json +4 -0
  49. data/skel/roles/all/tasks/00000-do-something-cool +27 -0
  50. data/skel/roles/bootstrap/files/cult-motd +45 -0
  51. data/skel/roles/bootstrap/role.json +4 -0
  52. data/skel/roles/bootstrap/tasks/00000-set-hostname +22 -0
  53. data/skel/roles/bootstrap/tasks/00001-add-cult-user +21 -0
  54. data/skel/roles/bootstrap/tasks/00002-install-cult-motd +9 -0
  55. metadata +183 -0
data/lib/cult/node.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'cult/role'
2
+ require 'fileutils'
3
+
4
+ module Cult
5
+ class Node < Role
6
+ def self.from_data!(project, data)
7
+ node = by_name(project, data[:name])
8
+ raise Errno::EEXIST if node.exist?
9
+
10
+ FileUtils.mkdir_p(node.path)
11
+ File.write(project.dump_name(node.node_path),
12
+ project.dump_object(data))
13
+ return by_name(project, data[:name])
14
+ end
15
+
16
+ # These are convenience methods for templates, etc.
17
+ # delegate them to the definition.
18
+ %i(user host ipv4_public ipv4_private ipv6_public ipv6_private).each do |m|
19
+ define_method(m) do
20
+ definition[m.to_s]
21
+ end
22
+ end
23
+
24
+
25
+ def self.path(project)
26
+ File.join(project.path, 'nodes')
27
+ end
28
+
29
+
30
+ def node_path
31
+ File.join(path, 'node')
32
+ end
33
+
34
+
35
+ def state_path
36
+ File.join(path, 'state')
37
+ end
38
+
39
+
40
+ def definition_path
41
+ [ node_path, state_path ]
42
+ end
43
+
44
+
45
+ def definition_parameters
46
+ super.merge(node: self)
47
+ end
48
+
49
+
50
+ def extra_file
51
+ File.join(path, 'extra')
52
+ end
53
+
54
+
55
+ def includes
56
+ definition.direct('roles') || super
57
+ end
58
+
59
+
60
+ alias_method :roles, :parent_roles
61
+ end
62
+ end
@@ -0,0 +1,169 @@
1
+ require 'securerandom'
2
+ require 'shellwords'
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ require 'cult/config'
7
+ require 'cult/role'
8
+ require 'cult/provider'
9
+
10
+ module Cult
11
+ class Project
12
+ CULT_RC = '.cultrc'
13
+
14
+ attr_reader :path
15
+ attr_accessor :cult_version
16
+
17
+ def initialize(path)
18
+ @path = path
19
+
20
+ if Cult.immutable?
21
+ self.provider
22
+ self.freeze
23
+ end
24
+ end
25
+
26
+
27
+ def name
28
+ File.basename(path)
29
+ end
30
+
31
+
32
+ def cultrc
33
+ location_of(CULT_RC)
34
+ end
35
+
36
+
37
+ def execute_cultrc
38
+ load(cultrc)
39
+ end
40
+
41
+
42
+ def inspect
43
+ "\#<#{self.class.name} name=#{name.inspect} path=#{path.inspect}>"
44
+ end
45
+ alias_method :to_s, :inspect
46
+
47
+
48
+ def location_of(file)
49
+ File.join(path, file)
50
+ end
51
+
52
+
53
+ def relative_path(obj_path)
54
+ prefix = "#{path}/"
55
+
56
+ if obj_path.start_with?(prefix)
57
+ return obj_path[prefix.length .. -1]
58
+ end
59
+
60
+ fail ArgumentError, "#{path} isn't in the project"
61
+ end
62
+
63
+
64
+ def remote_path
65
+ "cult"
66
+ end
67
+
68
+
69
+ def constructed?
70
+ File.exist?(cultrc)
71
+ end
72
+ alias_method :exist?, :constructed?
73
+
74
+ def nodes
75
+ @nodes ||= begin
76
+ Node.all(self)
77
+ end
78
+ end
79
+
80
+
81
+ def roles
82
+ @roles ||= begin
83
+ Role.all(self)
84
+ end
85
+ end
86
+
87
+
88
+ def providers
89
+ @providers ||= begin
90
+ Cult::Provider.all(self)
91
+ end
92
+ end
93
+
94
+ attr_writer :default_provider
95
+ def default_provider
96
+ @default_provider ||= providers[0]
97
+ end
98
+
99
+
100
+ def drivers
101
+ @drivers ||= begin
102
+ Cult::Drivers.all
103
+ end
104
+ end
105
+
106
+
107
+ def self.locate(path)
108
+ path = File.expand_path(path)
109
+ loop do
110
+ return nil if path == '/'
111
+
112
+ unless File.directory?(path)
113
+ path = File.dirname(path)
114
+ end
115
+
116
+ candidate = File.join(path, CULT_RC)
117
+ return new(path) if File.exist?(candidate)
118
+ path = File.dirname(path)
119
+ end
120
+ end
121
+
122
+
123
+ def self.from_cwd
124
+ locate Dir.getwd
125
+ end
126
+
127
+ attr_accessor :git_integration
128
+ alias_method :git?, :git_integration
129
+
130
+ def git_branch
131
+ res = %x(git -C #{Shellwords.escape(path)} branch --no-color)
132
+ if res && (m = res.match(/^\* (.*)/))
133
+ return m[1].chomp
134
+ end
135
+ end
136
+
137
+
138
+ def dump_yaml?
139
+ !! (ENV['CULT_DUMP'] || '').match(/^yaml$/i)
140
+ end
141
+
142
+
143
+ def dump_object(obj)
144
+ dump_yaml? ? YAML.dump(obj) : JSON.pretty_generate(obj)
145
+ end
146
+
147
+
148
+ def dump_name(basename)
149
+ basename + (dump_yaml? ? '.yml' : '.json')
150
+ end
151
+
152
+
153
+ def env
154
+ ENV['CULT_ENV'] || begin
155
+ if git_branch&.match(/\bdev(el(opment)?)?\b/)
156
+ 'development'
157
+ else
158
+ 'production'
159
+ end
160
+ end
161
+ end
162
+
163
+
164
+ def development?
165
+ env == 'development'
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,134 @@
1
+ require 'cult/named_array'
2
+ require 'cult/definition'
3
+
4
+ require 'forwardable'
5
+
6
+ module Cult
7
+ class Provider
8
+ extend Forwardable
9
+
10
+ def_delegators :driver, :sizes, :images, :zones, :provision!, :destroy!
11
+
12
+ attr_reader :project
13
+ attr_reader :path
14
+
15
+
16
+ def initialize(project, path)
17
+ @project = project
18
+ @path = path
19
+ end
20
+
21
+
22
+ def name
23
+ File.basename(path)
24
+ end
25
+
26
+
27
+ def inspect
28
+ prelude = "#{self.class.name} \"#{name}\""
29
+ driver_name = driver.class.driver_name
30
+ driver_string = (driver_name == name) ? '' : " driver=\"#{driver_name}\""
31
+ "\#<#{prelude}#{driver_string}>"
32
+ end
33
+
34
+
35
+ def driver
36
+ @driver ||= begin
37
+ cls = project.drivers[definition['driver']]
38
+ cls.new(api_key: definition['api_key'])
39
+ end
40
+ end
41
+
42
+
43
+ def definition
44
+ @definition ||= Definition.new(self)
45
+ end
46
+
47
+
48
+ def definition_path
49
+ [File.join(path, "provider"), File.join(path, "defaults")]
50
+ end
51
+
52
+
53
+ def definition_parameters
54
+ { project: self.project }
55
+ end
56
+
57
+ def definition_parents
58
+ []
59
+ end
60
+
61
+
62
+ # Chooses the smallest size setup with Ubuntu > Debian > Redhat,
63
+ # and a random zone.
64
+ def self.generate_defaults(definition)
65
+ definition = JSON.parse(definition.to_json)
66
+ text_to_mb = ->(text) {
67
+ multipliers = {
68
+ mb: 1 ** 1,
69
+ gb: 1 ** 2,
70
+ tb: 1 ** 3,
71
+ pb: 1 ** 4
72
+ }
73
+ if (m = text.match(/(\d+)([mgtp]b)/))
74
+ base = m[1].to_i
75
+ mul = multipliers.fetch(m[2].to_sym)
76
+ base * mul
77
+ else
78
+ nil
79
+ end
80
+ }
81
+
82
+ conf = definition['configurations']
83
+
84
+ # select the smallest size
85
+ size = conf['sizes'].map do |size|
86
+ if mb = text_to_mb.(size)
87
+ [mb, size]
88
+ else
89
+ nil
90
+ end
91
+ end.compact.sort_by(&:first).last.last
92
+
93
+ image = conf['images'].sort_by do |i|
94
+ case i
95
+ when /ubuntu-(\d+)-(\d+)/;
96
+ 10000 + ($1.to_i * 100) + ($2.to_i * 10)
97
+ when /debian-(\d+)/
98
+ 9000 + ($1.to_i * 100)
99
+ when /(redhat|centos|fedora)-(\d+)/
100
+ 8000 + ($2.to_i * 100)
101
+ else
102
+ 1
103
+ end
104
+ end.last
105
+
106
+ zone = conf['zones'].sample
107
+
108
+ {
109
+ default_size: size,
110
+ default_zone: zone,
111
+ default_image: image
112
+ }
113
+ end
114
+
115
+
116
+ def self.path(project)
117
+ project.location_of("providers")
118
+ end
119
+
120
+
121
+ def self.all_files(project)
122
+ Dir.glob(File.join(path(project), "*")).select do |file|
123
+ Dir.exist?(file)
124
+ end
125
+ end
126
+
127
+
128
+ def self.all(project)
129
+ all_files(project).map do |filename|
130
+ new(project, filename)
131
+ end.to_named_array
132
+ end
133
+ end
134
+ end
data/lib/cult/role.rb ADDED
@@ -0,0 +1,213 @@
1
+ require 'tsort'
2
+
3
+ require 'cult/task'
4
+ require 'cult/artifact'
5
+ require 'cult/config'
6
+ require 'cult/definition'
7
+ require 'cult/named_array'
8
+
9
+ module Cult
10
+ class Role
11
+ attr_accessor :project
12
+ attr_accessor :path
13
+
14
+ def initialize(project, path)
15
+ @project = project
16
+ @path = path
17
+
18
+ if Cult.immutable?
19
+ definition
20
+ parent_roles
21
+ self.freeze
22
+ end
23
+ end
24
+
25
+
26
+ def exist?
27
+ Dir.exist?(path)
28
+ end
29
+
30
+
31
+ def name
32
+ File.basename(path)
33
+ end
34
+
35
+
36
+ def collection_name
37
+ class_name = self.class.name.split('::')[-1]
38
+ class_name.downcase + 's'
39
+ end
40
+
41
+
42
+ def remote_path
43
+ File.join(project.remote_path, collection_name, name)
44
+ end
45
+
46
+
47
+ def relative_path(obj_path)
48
+ fail unless obj_path.start_with?(path)
49
+ obj_path[path.size + 1 .. -1]
50
+ end
51
+
52
+
53
+ def inspect
54
+ if Cult.immutable?
55
+ "\#<#{self.class.name} id:#{object_id.to_s(36)} #{name.inspect}>"
56
+ else
57
+ "\#<#{self.class.name} #{name.inspect}>"
58
+ end
59
+ end
60
+ alias_method :to_s, :inspect
61
+
62
+
63
+ def hash
64
+ [self.class, project, path].hash
65
+ end
66
+
67
+
68
+ def ==(rhs)
69
+ [self.class, project, path] == [rhs.class, rhs.project, rhs.path]
70
+ end
71
+ alias_method :eql?, :==
72
+
73
+
74
+ def tasks
75
+ Task.all_for_role(project, self)
76
+ end
77
+
78
+
79
+ def artifacts
80
+ Artifact.all_for_role(project, self)
81
+ end
82
+ alias_method :files, :artifacts
83
+
84
+
85
+ def definition
86
+ @definition ||= Definition.new(self)
87
+ end
88
+
89
+
90
+ def definition_path
91
+ File.join(path, "role")
92
+ end
93
+
94
+
95
+ def definition_parameters
96
+ { project: project, role: self }
97
+ end
98
+
99
+
100
+ def definition_parents
101
+ parent_roles
102
+ end
103
+
104
+
105
+ def includes
106
+ definition.direct('includes') || ['all']
107
+ end
108
+
109
+
110
+ def parent_roles
111
+ Array(includes).map do |name|
112
+ Role.by_name(project, name)
113
+ end.to_named_array
114
+ end
115
+
116
+
117
+ def recursive_parent_roles(seen = [])
118
+ result = []
119
+ parent_roles.each do |role|
120
+ next if seen.include?(role)
121
+ seen.push(role)
122
+ result.push(role)
123
+ result += role.recursive_parent_roles(seen)
124
+ end
125
+ result.to_named_array
126
+ end
127
+
128
+
129
+ def tree
130
+ ([self] + recursive_parent_roles).to_named_array
131
+ end
132
+
133
+
134
+ def self.by_name(project, name)
135
+ new(project, File.join(path(project), name))
136
+ end
137
+
138
+
139
+ def self.path(project)
140
+ File.join(project.path, "roles")
141
+ end
142
+
143
+
144
+ def self.all_files(project)
145
+ Dir.glob(File.join(path(project), "*")).select do |file|
146
+ Dir.exist?(file)
147
+ end
148
+ end
149
+
150
+
151
+ if Cult.immutable?
152
+ def self.cache_get(cls, *args)
153
+ @singletons ||= {}
154
+ key = [cls, *args]
155
+
156
+ if (rval = @singletons[key])
157
+ return rval
158
+ end
159
+
160
+ return nil
161
+ end
162
+
163
+
164
+ def self.cache_put(obj, *args)
165
+ @singletons ||= {}
166
+ key = [obj.class, *args]
167
+ @singletons[key] = obj
168
+ obj
169
+ end
170
+
171
+
172
+ def self.new(*args)
173
+ if (result = cache_get(self, *args))
174
+ return result
175
+ else
176
+ result = super
177
+ cache_put(result, *args)
178
+ return result
179
+ end
180
+ end
181
+ end
182
+
183
+
184
+ def self.all(project)
185
+ all_files(project).map do |filename|
186
+ new(project, filename).tap do |new_role|
187
+ yield new_role if block_given?
188
+ end
189
+ end.to_named_array
190
+ end
191
+
192
+
193
+ def build_order
194
+ all_items = [self] + parent_roles
195
+
196
+ each_node = ->(&block) {
197
+ all_items.each(&block)
198
+ }
199
+
200
+ each_child = ->(node, &block) {
201
+ node.parent_roles.each(&block)
202
+ }
203
+
204
+ TSort.tsort(each_node, each_child).to_named_array
205
+ end
206
+
207
+
208
+ def has_role?(role)
209
+ ! tree[role].nil?
210
+ end
211
+
212
+ end
213
+ end
data/lib/cult/skel.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'fileutils'
2
+ require 'cult/template'
3
+
4
+ module Cult
5
+ class Skel
6
+ SKEL_DIR = File.expand_path(File.join(__dir__, '../../skel'))
7
+
8
+ attr_reader :project
9
+
10
+ def initialize(project)
11
+ @project = project
12
+ end
13
+
14
+
15
+ def template
16
+ @erb ||= Template.new(project: project)
17
+ end
18
+
19
+
20
+ # Skeleton files are files that are copied over for a new project.
21
+ # We allow template files to live in the skeleton directory too, but
22
+ # they're not copied over until needed.
23
+ def skeleton_files
24
+ Dir.glob(File.join(SKEL_DIR, "**", "{.*,*}")).reject do |fn|
25
+ fn.match(/template/i)
26
+ end
27
+ end
28
+
29
+
30
+ def template_file(name)
31
+ File.join(SKEL_DIR, name)
32
+ end
33
+
34
+
35
+ def copy_template(name, dst)
36
+ src = template_file(name)
37
+ dst = project.location_of(dst)
38
+ process_file(src, dst)
39
+ end
40
+
41
+
42
+ def process_file(src, dst = nil)
43
+ dst ||= begin
44
+ relative = src.sub(%r/\A#{Regexp.escape(SKEL_DIR)}/, '')
45
+ project.location_of(relative)
46
+ end
47
+
48
+ if File.directory?(src)
49
+ return
50
+ end
51
+
52
+ dst, data = case src
53
+ when /\.erb\z/
54
+ [ dst.sub(/\.erb\z/, ''), template.process(File.read(src))]
55
+ else
56
+ [ dst, File.read(src) ]
57
+ end
58
+
59
+ display_name = File.basename(dst) == ".keep" ? File.dirname(dst) : dst
60
+
61
+ print " Creating #{display_name}"
62
+ if File.exist?(dst)
63
+ puts " exists, skipped."
64
+ return
65
+ end
66
+
67
+
68
+ FileUtils.mkdir_p(File.dirname(dst))
69
+
70
+ File.write(dst, data)
71
+ File.chmod(File.stat(src).mode, dst)
72
+ puts
73
+ end
74
+
75
+
76
+ def copy!
77
+ puts "Creating project from skeleton..."
78
+ FileUtils.mkdir_p(project.path)
79
+ skeleton_files.each do |file|
80
+ process_file(file)
81
+ end
82
+ puts
83
+ end
84
+ end
85
+ end
data/lib/cult/task.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'cult/transferable'
2
+ require 'cult/named_array'
3
+
4
+ module Cult
5
+ class Task
6
+ include Transferable
7
+
8
+ attr_reader :path
9
+ attr_reader :role
10
+ attr_reader :serial
11
+ attr_reader :name
12
+
13
+ LEADING_ZEROS = 5
14
+ BASENAME_RE = /\A(\d{#{LEADING_ZEROS},})-([\w-]+)(\..+)?\z/i
15
+
16
+
17
+ def initialize(role, path)
18
+ @role = role
19
+ @path = path
20
+ @basename = File.basename(path)
21
+
22
+ if (m = @basename.match(BASENAME_RE))
23
+ @serial = m[1].to_i
24
+ @name = m[2]
25
+ else
26
+ fail ArgumentError, "invalid task name: #{path}"
27
+ end
28
+ end
29
+
30
+
31
+ def self.from_serial_and_name(role, serial:, name:)
32
+ basename = sprintf("%0#{LEADING_ZEROS}d-%s", serial, name)
33
+ new(role, File.join(role.path, collection_name, basename))
34
+ end
35
+
36
+
37
+ def relative_path
38
+ File.basename(path)
39
+ end
40
+
41
+
42
+ def inspect
43
+ "\#<#{self.class.name} role:#{role&.name.inspect} " +
44
+ "serial:#{serial} name:#{name.inspect}>"
45
+ end
46
+ alias_method :to_s, :inspect
47
+
48
+
49
+ def file_mode
50
+ super | 0100
51
+ end
52
+
53
+
54
+ def self.all_for_role(project, role)
55
+ Dir.glob(File.join(role.path, "tasks", "*")).map do |filename|
56
+ next unless File.basename(filename).match(BASENAME_RE)
57
+ new(role, filename).tap do |new_task|
58
+ yield new_task if block_given?
59
+ end
60
+ end.compact.to_named_array
61
+ end
62
+
63
+ end
64
+ end