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
@@ -0,0 +1,205 @@
1
+ require 'cult/skel'
2
+ require 'cult/commander'
3
+
4
+ require 'SecureRandom'
5
+
6
+ module Cult
7
+ module CLI
8
+
9
+ module_function
10
+ def node_cmd
11
+ node = Cri::Command.define do
12
+ optional_project
13
+ name 'node'
14
+ aliases 'nodes'
15
+ summary 'Manage nodes'
16
+ description <<~EOD.format_description
17
+ The node commands manipulate your local index of nodes. A node is
18
+ conceptually description of a server.
19
+ EOD
20
+
21
+ run(arguments: 0) do |opts, args, cmd|
22
+ puts cmd.help
23
+ end
24
+ end
25
+
26
+ node_ssh = Cri::Command.define do
27
+ name 'ssh'
28
+ usage 'ssh NODE'
29
+ summary 'Starts an interactive SSH shell to NODE'
30
+ description <<~EOD.format_description
31
+ EOD
32
+
33
+ run(arguments: 1) do |opts, args, cmd|
34
+ node = CLI.fetch_item(args[0], from: Node)
35
+ exec "ssh", "#{node.user}@#{node.host}"
36
+ end
37
+ end
38
+ node.add_command(node_ssh)
39
+
40
+ node_create = Cri::Command.define do
41
+ name 'create'
42
+ aliases 'new'
43
+ usage 'create [options] NAME...'
44
+ summary 'Create a new node'
45
+ description <<~EOD.format_description
46
+ This command creates a new node specification. With --bootstrap,
47
+ it'll also provision it, so it'll actually exist out there.
48
+
49
+ The newly created node will have all the roles listed in --role. If
50
+ none are specified, it'll have the role "all". If no name is
51
+ provided, it will be named for its role(s).
52
+
53
+ If multiple names are provided, a new node is created for each name
54
+ given.
55
+
56
+ The --count option lets you create an arbitrary amount of new nodes.
57
+ The nodes will be identical, except they'll be named with increasing
58
+ sequential numbers, like:
59
+
60
+ > web-01, web-02
61
+
62
+ And so forth. The --count option is incompatible with multiple names
63
+ given on the command line. If --count is specified with one name, the
64
+ name will become the prefix for all nodes created. If --count is
65
+ specified with no names, the prefix will be generated from the role
66
+ names used, as discussed above.
67
+ EOD
68
+
69
+ required :r, :role, 'Specify possibly multiple roles',
70
+ multiple: true
71
+ required :n, :count, 'Generates <value> number of nodes'
72
+
73
+ required :p, :provider, 'Provider'
74
+ required :Z, :zone, 'Provider zone'
75
+ required :I, :image, 'Provider image'
76
+ required :S, :size, 'Provider instance size'
77
+
78
+ flag :b, :bootstrap, 'Bring up node'
79
+
80
+ run(arguments: 0..-1) do |opts, args, cmd|
81
+ random_suffix = ->(basename) do
82
+ begin
83
+ suffix = CLI.unique_id
84
+ CLI.fetch_item("#{basename}-#{suffix}", from: Node, exist: false)
85
+ rescue CLIError
86
+ retry
87
+ end
88
+ end
89
+
90
+ generate_sequenced_names = ->(name, n) do
91
+ result = []
92
+ result.push(random_suffix.(name)) until result.size == n
93
+ result
94
+ end
95
+
96
+ unless opts[:count].nil? || opts[:count].match(/^\d+$/)
97
+ fail CLIError, "--count must be an integer"
98
+ end
99
+
100
+ names = args.dup
101
+
102
+ roles = opts[:role] ? CLI.fetch_items(opts[:role], from: Role) : []
103
+
104
+ if roles.empty?
105
+ roles = CLI.fetch_items('all', from: Role)
106
+ if names.empty?
107
+ begin
108
+ names.push CLI.fetch_item('node', from: Node, exist: false)
109
+ rescue
110
+ names.push random_suffix.('node')
111
+ end
112
+ end
113
+ end
114
+
115
+ if names.size > 1 && opts[:count]
116
+ fail CLIError, "cannot specify both --count and more than one name"
117
+ end
118
+
119
+ if names.empty? && !roles.empty?
120
+ names.push roles.map(&:name).sort.join('-')
121
+ end
122
+
123
+ if opts[:count]
124
+ names = generate_sequenced_names.(names[0], opts[:count].to_i)
125
+ end
126
+
127
+ # Makes sure they're all new.
128
+ names = names.map do |name|
129
+ CLI.fetch_item(name, from: Node, exist: false)
130
+ end
131
+
132
+ provider = if opts.key?(:provider)
133
+ CLI.fetch_item(opts[:provider], from: Provider)
134
+ else
135
+ Cult.project.default_provider
136
+ end
137
+
138
+ # Use --size if it was specified, otherwise pull the
139
+ # provider's default.
140
+ node_spec = %i(size image zone).map do |m|
141
+ value = opts[m] || provider.definition["default_#{m}"]
142
+ fail CLIError, "No #{m} specified (and no default)" if value.nil?
143
+ [m, value]
144
+ end.to_h
145
+
146
+ nodes = names.map do |name|
147
+ data = {
148
+ name: name,
149
+ roles: roles.map(&:name)
150
+ }
151
+
152
+ Node.from_data!(Cult.project, data).tap do |node|
153
+ if opts[:bootstrap]
154
+ prov_data = provider.provision!(name: node.name,
155
+ image: node_spec[:image],
156
+ size: node_spec[:size],
157
+ zone: node_spec[:zone],
158
+ ssh_key_files: '/Users/mike/.ssh/id_rsa.pub')
159
+ File.write(Cult.project.dump_name(node.state_path),
160
+ Cult.project.dump_object(prov_data))
161
+
162
+ c = Commander.new(project: Cult.project, node: node)
163
+ c.bootstrap!
164
+ c.install!(node)
165
+ end
166
+ end
167
+ end
168
+
169
+ end
170
+ end
171
+ node.add_command node_create
172
+
173
+ node_list = Cri::Command.define do
174
+ name 'list'
175
+ aliases 'ls'
176
+ summary 'List nodes'
177
+ description <<~EOD.format_description
178
+ This command lists the nodes in the project.
179
+ EOD
180
+
181
+ required :r, :role, 'List only nodes which include <value>',
182
+ multiple: true
183
+
184
+ run(arguments: 0..1) do |opts, args, cmd|
185
+ nodes = args.empty? ? Cult.project.nodes
186
+ : CLI.fetch_items(*args, from: Node)
187
+
188
+ if opts[:role]
189
+ roles = CLI.fetch_items(opts[:role], from: Role)
190
+ nodes = nodes.select do |n|
191
+ roles.any? { |role| n.has_role?(role) }
192
+ end
193
+ end
194
+
195
+ nodes.each do |node|
196
+ puts "Node: #{node.inspect}"
197
+ end
198
+ end
199
+ end
200
+ node.add_command node_list
201
+
202
+ return node
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,123 @@
1
+ require 'cult/drivers/load'
2
+
3
+ module Cult
4
+ module CLI
5
+
6
+ module_function
7
+ def provider_cmd
8
+ provider = Cri::Command.define do
9
+ optional_project
10
+ name 'provider'
11
+ aliases 'providers'
12
+ summary 'Provider Commands'
13
+ description <<~EOD.format_description
14
+ A provider is a VPS service. Cult ships with drivers for quite a few
15
+ services, (which can be listed with `cult provider drivers`).
16
+
17
+ The commands here actually set up your environment with provider
18
+ accounts. Regarding terminology:
19
+
20
+ A "driver" is an interface to a third party service you probably pay
21
+ for, for example, "mikes-kvm-warehouse" would be a driver that knows
22
+ how to interact with the commercial VPS provider "Mike's KVM
23
+ Warehouse".
24
+
25
+ A "provider" is a configured account on a service, which uses a
26
+ driver to get things done. For example "Bob's Account at Mike's
27
+ KVM Warehouse".
28
+
29
+ In a lot the common case, you'll be using one provider, which is using
30
+ a driver of the same name.
31
+ EOD
32
+
33
+ run(arguments: 0) do |opts, args, cmd|
34
+ puts cmd.help
35
+ end
36
+ end
37
+
38
+
39
+ provider_list = Cri::Command.define do
40
+ name 'list'
41
+ aliases 'ls'
42
+ summary 'List Providers'
43
+ description <<~EOD.format_description
44
+ Lists Providers for this project. If --driver is specified, it only
45
+ lists Providers which employ that driver.
46
+ EOD
47
+ required :d, :driver, "Restrict list to providers using DRIVER"
48
+
49
+ run(arguments: 0..1) do |opts, args, cmd|
50
+ providers = Cult.project.providers
51
+
52
+ # Filtering
53
+ providers = providers.all(args[0]) if args[0]
54
+
55
+ if opts[:driver]
56
+ driver_cls = Cult.project.drivers[opts[:driver]]
57
+ providers = providers.select do |p|
58
+ p.driver.is_a?(driver_cls)
59
+ end
60
+ end
61
+
62
+ providers.each do |p|
63
+ printf "%-20s %-s\n", p.name, Cult.project.relative_path(p.path)
64
+ end
65
+
66
+ end
67
+ end
68
+ provider.add_command(provider_list)
69
+
70
+
71
+ provider_avail = Cri::Command.define do
72
+ optional_project
73
+ name 'drivers'
74
+ summary 'list available drivers'
75
+ description <<~EOD.format_description
76
+ Displays a list of all available drivers, by their name, and list of
77
+ gem dependencies.
78
+ EOD
79
+
80
+ run(arguments: 0) do |opts, args, cmd|
81
+ Cult::Drivers.all.each do |p|
82
+ printf "%-20s %-s\n", p.driver_name, p.required_gems
83
+ end
84
+ end
85
+ end
86
+ provider.add_command(provider_avail)
87
+
88
+
89
+ provider_create = Cri::Command.define do
90
+ name 'create'
91
+ aliases 'new'
92
+ usage 'create NAME'
93
+ summary 'creates a new provider for your project'
94
+ required :d, :driver, 'Specify driver, if different than NAME'
95
+ description <<~EOD.format_description
96
+ Creates a new provider for the project. There are a few ways this
97
+ can be specified, for example
98
+
99
+ cult provider create mikes-kvm-warehouse
100
+
101
+ Will set up a provider account using 'mikes-kvm-warehouse' as both
102
+ the driver type and the local provider name.
103
+
104
+ If you need the two to be separate, for example, if you have multiple
105
+ accounts at Mike's KVM Warehouse, you can specify a driver name with
106
+ --driver, and an independent provider name.
107
+ EOD
108
+
109
+ run(arguments: 1) do |opts, args, cmd|
110
+ name, _ = *args
111
+ driver = CLI.fetch_item(opts[:driver] || name, from: Driver)
112
+ name = CLI.fetch_item(name, from: Provider, exist: false)
113
+
114
+ puts [driver, name].inspect
115
+ end
116
+ end
117
+ provider.add_command(provider_create)
118
+
119
+
120
+ provider
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,149 @@
1
+ require 'fileutils'
2
+
3
+ module Cult
4
+ module CLI
5
+ module_function
6
+
7
+ def role_cmd
8
+ role = Cri::Command.define do
9
+ optional_project
10
+ name 'role'
11
+ aliases 'roles'
12
+ summary 'Manage roles'
13
+ description <<~EOD.format_description
14
+ A role defines what a node does. The easiest way to think about it
15
+ is just a directory full of scripts (tasks).
16
+
17
+ A role can include an arbitrary number of other roles. For example,
18
+ you may have two roles `rat-site' and `tempo-site', which both depend
19
+ on a common role `web-server'. In this case, `web-server' would be
20
+ the set of tasks that install the web server through the package
21
+ manager, set up a base configuration, and allow ports 80 and 443
22
+ through the firewall.
23
+
24
+ Both `rat-site' and `tempo-site' would declare that they depend on
25
+ `web-server' by listing it in their `includes' array in role.json.
26
+ Their tasks would then only consist of dropping a configuration file,
27
+ TLS keys and certificates into `/etc/your-httpd.d`.
28
+
29
+ Composability is the mindset behind roles. Cult assumes, by default,
30
+ that roles are written in a way to compose well with each other if
31
+ they find themselves on the same node. That is not always possible,
32
+ (thus the `conflicts' key exists in `role.json'), but is the goal.
33
+ You should write tasks with that in mind. For example, dropping
34
+ files into `/etc/your-httpd.d` instead of re-writing
35
+ `/etc/your-httpd.conf`. With this setup, a node could include both
36
+ `rat-site` and `tempo-site` roles and be happily serving both sites.
37
+
38
+ By default, `cult init` generates two root roles that don't depend on
39
+ anything else: `all` and `bootstrap`. The `bootstrap` role exists
40
+ to get a node from a clean OS install to a configuration to be
41
+ managed by the settings in `all'. Theoretically, if you're happy
42
+ doing all deploys as the root user, you don't need a `bootstrap` role
43
+ at all: Delete it and set the `user` key in `all/role.json` to
44
+ "root".
45
+
46
+ The tasks in the `all` role are considered shared amongst all roles.
47
+ However, the only thing special about the `all` role is that Cult
48
+ assumes roles and nodes without an explicit `includes` setting belong
49
+ to all.
50
+ EOD
51
+
52
+ run(arguments: 0) do |opts, args, cmd|
53
+ puts cmd.help
54
+ end
55
+ end
56
+
57
+
58
+ role_create = Cri::Command.define do
59
+ name 'create'
60
+ aliases 'new'
61
+ summary 'creates a new role'
62
+ usage 'create [options] NAME'
63
+ description <<~EOD.format_description
64
+ Creates a new role names NAME, which will then be available under
65
+ $CULT_PROJECT/roles/$NAME
66
+ EOD
67
+
68
+ required :r, :roles, 'this role depends on another role',
69
+ multiple: true
70
+
71
+ run(arguments: 1) do |opts, args, cmd|
72
+ name = CLI.fetch_item(args[0], from: Role, exist: false)
73
+
74
+ role = Role.by_name(Cult.project, name)
75
+ data = {}
76
+
77
+ if opts[:roles]
78
+ data[:includes] = CLI.fetch_items(opts[:roles],
79
+ from: Role).map(&:name)
80
+ end
81
+ FileUtils.mkdir_p(role.path)
82
+ File.write(Cult.project.dump_name(role.definition_file),
83
+ Cult.project.dump_object(data))
84
+
85
+ FileUtils.mkdir_p(File.join(role.path, "files"))
86
+ File.write(File.join(role.path, "files", ".keep"), '')
87
+
88
+ FileUtils.mkdir_p(File.join(role.path, "tasks"))
89
+ File.write(File.join(role.path, "tasks", ".keep"), '')
90
+ end
91
+ end
92
+ role.add_command role_create
93
+
94
+
95
+ role_destroy = Cri::Command.define do
96
+ name 'destroy'
97
+ aliases 'delete', 'rm'
98
+ usage 'destroy ROLES...'
99
+ summary 'Destroy role ROLE'
100
+ description <<~EOD.format_description
101
+ Destroys all roles specified.
102
+ EOD
103
+
104
+ run(arguments: 1..-1) do |opts, args, cmd|
105
+ roles = args.map do |role_name|
106
+ CLI.fetch_items(role_name, from: Role)
107
+ end.flatten
108
+
109
+ roles.each do |role|
110
+ if CLI.yes_no?("Delete role #{role.name} (#{role.path})?",
111
+ default: :no)
112
+ FileUtils.rm_rf(role.path)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ role.add_command(role_destroy)
118
+
119
+
120
+ role_list = Cri::Command.define do
121
+ name 'list'
122
+ aliases 'ls'
123
+ usage 'list [ROLES...]'
124
+ summary 'List existing roles'
125
+ description <<~EOD.format_description
126
+ Lists roles in this project. By default, lists all roles. If one or
127
+ more ROLES are specified, only lists those
128
+ EOD
129
+
130
+ run(arguments: 0..-1) do |opts, args, cmd|
131
+ roles = Cult.project.roles
132
+ unless args.empty?
133
+ roles = CLI.fetch_items(*args, from: Role)
134
+ end
135
+
136
+ roles.each do |r|
137
+ fmt = "%-20s %s\n"
138
+ printf fmt, r.name, r.build_order.map(&:name).join(', ')
139
+ end
140
+
141
+ end
142
+ end
143
+ role.add_command(role_list)
144
+
145
+
146
+ role
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,140 @@
1
+ require 'cult/task'
2
+ require 'cult/role'
3
+
4
+ module Cult
5
+ module CLI
6
+ module_function
7
+ def task_cmd
8
+ task = Cri::Command.define do
9
+ optional_project
10
+ name 'task'
11
+ aliases 'tasks'
12
+ summary 'Task Manipulation'
13
+ usage 'task [command]'
14
+ description <<~EOD.format_description
15
+
16
+ EOD
17
+
18
+ run(arguments: 0) do |opts, args, cmd|
19
+ puts cmd.help
20
+ exit
21
+ end
22
+ end
23
+
24
+
25
+ task_reserial = Cri::Command.define do
26
+ name 'resequence'
27
+ summary 'Resequences task serial numbers'
28
+
29
+ flag :A, :all, 'Reserial all roles'
30
+ flag :G, :'git-add', '`git add` each change'
31
+ required :r, :role, 'Roles to resequence (multiple)',
32
+ multiple: true
33
+
34
+ description <<~EOD.format_description
35
+ Resequences the serial numbers in each task provided with --roles,
36
+ or all roles with --all. You cannot supply both --all and specify
37
+ --roles.
38
+
39
+ A resequence isn't something to do lightly once you have deployed
40
+ nodes. This will be elaborated on in the future. It's probably
41
+ a good idea to do this in a development branch and test out the
42
+ results.
43
+
44
+ The --git-add option will execute `git add` for each rename made.
45
+ This will make your status contain a bunch of neat renames, instead of
46
+ a lot of deleted and untracked files.
47
+
48
+ This command respects the global --yes flag.
49
+ EOD
50
+
51
+
52
+ run(arguments: 0) do |opts, args, cmd|
53
+ if opts[:all] && Array(opts[:role]).size != 0
54
+ fail CLIError, "can't supply -A and also a list of roles"
55
+ end
56
+
57
+ roles = if opts[:all]
58
+ Cult.project.roles
59
+ elsif opts[:role]
60
+ CLI.fetch_items(opts[:role], from: Role)
61
+ else
62
+ fail CLIError, "no roles specified with --role or --all"
63
+ end
64
+
65
+ roles.each do |role|
66
+ puts "Resequencing role: `#{role.name}'"
67
+ tasks = role.tasks.sort_by do |task|
68
+ # This makes sure we don't change order for duplicate serials
69
+ [task.serial, task.name]
70
+ end
71
+
72
+ renames = tasks.map.with_index do |task, i|
73
+ if task.serial != i
74
+ new_task = Task.from_serial_and_name(role,
75
+ serial: i,
76
+ name: task.name)
77
+ [task, new_task]
78
+ end
79
+ end.compact.to_h
80
+
81
+ next if renames.empty?
82
+
83
+ unless Cult::CLI.yes?
84
+ renames.each do |src, dst|
85
+ puts "rename #{Cult.project.relative_path(src.path)} " +
86
+ "-> #{Cult.project.relative_path(dst.path)}"
87
+ end
88
+ end
89
+
90
+ if Cult::CLI.yes_no?("Execute renames?")
91
+ renames.each do |src, dst|
92
+ FileUtils.mv(src.path, dst.path)
93
+ if opts[:'git-add']
94
+ `git add #{src.path}; git add #{dst.path}`
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ task.add_command(task_reserial)
102
+
103
+
104
+ task_sanity = Cri::Command.define do
105
+ name 'sanity'
106
+ summary 'checks task files for numbering sanity'
107
+ description <<~EOD.format_description
108
+ TODO: Document (and do something!)
109
+ EOD
110
+
111
+ run do |opts, args, cmd|
112
+ puts 'checking sanity...'
113
+ end
114
+ end
115
+ task.add_command task_sanity
116
+
117
+
118
+ task_create = Cri::Command.define do
119
+ name 'create'
120
+ aliases 'new'
121
+ usage 'create [options] DESCRIPTION'
122
+ summary 'create a new task for ROLE with a proper serial'
123
+ description <<~EOD.format_description
124
+ EOD
125
+
126
+ required :r, :role, 'role for task. defaults to "all"'
127
+ flag :e, :edit, 'open generated task file in your $EDITOR'
128
+
129
+ run do |opts, args, cmd|
130
+ english = args.join " "
131
+ opts[:roles] ||= 'all'
132
+ puts [english, opts[:roles], opts[:edit]].inspect
133
+ end
134
+ end
135
+ task.add_command task_create
136
+
137
+ task
138
+ end
139
+ end
140
+ end