cult 0.1.1.pre

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