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