jisota 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/Guardfile +7 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +254 -0
  8. data/Rakefile +1 -0
  9. data/jisota.gemspec +30 -0
  10. data/lib/jisota/collection.rb +52 -0
  11. data/lib/jisota/command_script.rb +17 -0
  12. data/lib/jisota/composite_script.rb +23 -0
  13. data/lib/jisota/configuration.rb +60 -0
  14. data/lib/jisota/errors.rb +3 -0
  15. data/lib/jisota/file_script.rb +21 -0
  16. data/lib/jisota/logger.rb +74 -0
  17. data/lib/jisota/package.rb +35 -0
  18. data/lib/jisota/package_script.rb +54 -0
  19. data/lib/jisota/packages/apt.rb +17 -0
  20. data/lib/jisota/packages/ruby.rb +29 -0
  21. data/lib/jisota/param.rb +34 -0
  22. data/lib/jisota/param_parser.rb +70 -0
  23. data/lib/jisota/provisioner.rb +24 -0
  24. data/lib/jisota/role.rb +13 -0
  25. data/lib/jisota/script_block.rb +66 -0
  26. data/lib/jisota/server.rb +12 -0
  27. data/lib/jisota/ssh_engine.rb +11 -0
  28. data/lib/jisota/ssh_session.rb +48 -0
  29. data/lib/jisota/upload_file.rb +3 -0
  30. data/lib/jisota/version.rb +3 -0
  31. data/lib/jisota.rb +63 -0
  32. data/spec/acceptance/simple_script_spec.rb +56 -0
  33. data/spec/lib/jisota/collection_spec.rb +44 -0
  34. data/spec/lib/jisota/command_script_spec.rb +22 -0
  35. data/spec/lib/jisota/composite_script_spec.rb +37 -0
  36. data/spec/lib/jisota/configuration_spec.rb +82 -0
  37. data/spec/lib/jisota/file_script_spec.rb +22 -0
  38. data/spec/lib/jisota/logger_spec.rb +34 -0
  39. data/spec/lib/jisota/package_script_spec.rb +43 -0
  40. data/spec/lib/jisota/package_spec.rb +74 -0
  41. data/spec/lib/jisota/packages/apt_spec.rb +15 -0
  42. data/spec/lib/jisota/packages/ruby_spec.rb +14 -0
  43. data/spec/lib/jisota/role_spec.rb +31 -0
  44. data/spec/lib/jisota/script_block_spec.rb +51 -0
  45. data/spec/lib/jisota/server_spec.rb +37 -0
  46. data/spec/lib/jisota_spec.rb +34 -0
  47. data/spec/spec_helper.rb +21 -0
  48. data/spec/test_files/foo +1 -0
  49. metadata +221 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 868630ce53b3e30dee642175d2e3f9269c40749c
4
+ data.tar.gz: 692a72ec3e1df5c55e7c679837548584096c3bb0
5
+ SHA512:
6
+ metadata.gz: e48e212a2175b404f1cfd1d2b122841051902f3f868509b1321096a9e1315440da686113cf8f7b209b872028ed068d50af947dbf3d8e2c97746f80987dea3b73
7
+ data.tar.gz: 71963bcbed7a13a30aadd4bc17a2bace65ea8ff694924999213c0a63cdf9e2b72b727d41d515dd69e535212e1fe042d0be28f6a5e26d022a8e3d6fba2398657c
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ sandi_meter/
19
+ examples/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jisota.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ guard :rspec, cmd: "bundle exec rspec" do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ watch(%r{^spec/shared/.+}) { "spec" }
6
+ end
7
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Lasse Skindstad Ebert
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # Jisota
2
+
3
+ Jisota is a simple provisioning tool meant for smaller projects.
4
+
5
+ Provisioning, in this context, is the act of turning an empty server
6
+ into a working machine tailored to your needs.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'jisota', require: false
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install jisota
21
+
22
+ ## Introduction
23
+
24
+ A simple example of a Jisota script:
25
+
26
+ # config/provision.rb (Or whatever you want to call this file)
27
+ require 'jisota'
28
+
29
+ config = Jisota.config do
30
+ role :app do
31
+ ruby version: "2.1.1"
32
+ nginx config_file: "path/to/nginx.conf"
33
+ end
34
+
35
+ server "mydomain.com", user: "john_doe", roles: :app
36
+ end
37
+
38
+ Jisota.run(config)
39
+
40
+ Run the script with the ruby executable:
41
+
42
+ $ ruby config/provision.rb
43
+
44
+ ## Defining a package
45
+
46
+ Inside a `Jisota.config` block, use the `package` method to define a package.
47
+
48
+ Example:
49
+
50
+ Jisota.config do
51
+ package :essentials do
52
+ description "Installs important stuff"
53
+ run do
54
+ apt :curl, :vim, :git
55
+ end
56
+ end
57
+ end
58
+
59
+ In this simple example we define a package with the name `:essentials`. It has
60
+ a description and runs another package, `:apt`, that it calls with the arguments
61
+ `:curl, :vim, :git`
62
+
63
+ The DSL you can use inside a package include:
64
+
65
+ * `description`: Will provide a description for the package.
66
+ * `param`: Specify one or more named parameters for the package. See more in
67
+ the params section below.
68
+ * `run(&block)`: This block will be called when the package is executed.
69
+ * `verify(&block)`: If this block exits with code 0, the run block will not be executed
70
+
71
+ Inside a run-block, packages can be called by their name, somewhat like a Ruby
72
+ method. See the `apt` example above.
73
+
74
+ ### Defining packages to be used in more than one project
75
+
76
+ Defining packages with `Jisota.config` as above will only add the package to
77
+ the current configuration. To make packages global they need to be in the
78
+ global package manager. Simply use the `Jisota.global_config` instead:
79
+
80
+
81
+ Jisota.global_config do
82
+ package :ruby do
83
+ description "Installs ruby from source"
84
+ ...
85
+ end
86
+ end
87
+
88
+ This will make the package accessable from all configurations. If a local
89
+ package has the same name, the local package is used.
90
+
91
+ All build-in packages are defined this way.
92
+
93
+ ## Defining params for a package
94
+
95
+ Params for a package are defined with the `param` DSL method inside a
96
+ `package`-block. Params are named:
97
+
98
+ package :my_package do
99
+ param :foo
100
+ param :bar
101
+ ...
102
+ end
103
+
104
+ This package can be called either with named parameters or with sorted parameters.
105
+ The following calls are all equivalent:
106
+
107
+ my_package 42, "Baz"
108
+ my_package 42, bar: "Baz"
109
+ my_package foo: 42, bar: "Baz"
110
+ my_package bar: "Baz", foo: 42
111
+
112
+ ### Param options
113
+
114
+ Params can have the following options:
115
+
116
+ * `default`: Sets a default value, if no value is passed to that parameter
117
+ * `required`: Validates that the parameter will have a value
118
+ * `splat`: Will assign all the following unnamed arguments to this parameter as an array
119
+
120
+ Example:
121
+
122
+ package :apt do
123
+ param :packages, splat: true
124
+ param :command, default: "sudo apt-get install :package"
125
+ run do
126
+ cmd command.gsub(/:package/, packages.join(' '))
127
+ end
128
+ end
129
+
130
+ # And then call params inside another script block:
131
+ run do
132
+ apt :vim, :git
133
+ apt :vim, :git, command: "sudo apt-get install -y :package"
134
+ end
135
+
136
+
137
+ ## Atomic script operations
138
+
139
+ All packages will eventually boil down to these few atomic operations, which
140
+ can be called inside a script block:
141
+
142
+ ### cmd
143
+
144
+ Will run a script on the server. Example:
145
+
146
+ cmd "apt-get install foo"
147
+
148
+ ### upload
149
+
150
+ Uploads the file to the server. Example:
151
+
152
+ upload from: "path/to/nginx.conf", to: "/etc/nginx.conf"
153
+
154
+ ## Complete list of build-in packages
155
+
156
+ ### ruby
157
+
158
+ description "Installs ruby from source"
159
+ param :version, required: true
160
+ param :tmp_dir, default: "~/tmp"
161
+
162
+ ### apt
163
+
164
+ description "Installs packages with apt-get"
165
+ param :packages, required: true, splat: true
166
+
167
+ ### More packages?
168
+
169
+ Jisota is a young gem. Please contribute with any packages that you think
170
+ others could benifit from.
171
+
172
+ ## To DSL or not to DSL
173
+
174
+ The DSL provided by Jisota is just a layer of abstraction. The entire library
175
+ can easily be used without the DSL. This could be useful if you need to e.g.
176
+ dynamically build a configuration.
177
+
178
+ Example:
179
+
180
+ my_package = Jisota::Package.new(:stuff)
181
+ my_package.params << Jisota::Param.new(:foo, default: 42)
182
+
183
+ config = Jisota::Configuration.new
184
+ config.packages << my_package
185
+
186
+ is equivalent to:
187
+
188
+ config = Jisota.config do
189
+ package :stuff do
190
+ param :foo, default: 42
191
+ end
192
+ end
193
+
194
+ ## Full example
195
+
196
+ An example using most of the features of Jisota:
197
+
198
+ require 'jisota'
199
+
200
+ config = Jisota.config do
201
+ package :essentials do
202
+ description "Install essentials"
203
+ param :extra_packages, splat: true
204
+ run do
205
+ packages = %w[git vim curl]
206
+ packages += extra_packages
207
+ cmd "sudo apt-get install #{packages.join(" ")}"
208
+ end
209
+ end
210
+
211
+ package :postgres do
212
+ description "Installs postgres"
213
+ run do
214
+ cmd "some command sequence to install postgres"
215
+ end
216
+ end
217
+
218
+ package :nginx do
219
+ description "Install nginx"
220
+ param :config_file, required: true
221
+ run do
222
+ cmd "sudo apt get install nginx"
223
+ upload from: config_file, to: /etc/nginx.conf
224
+ end
225
+ end
226
+
227
+ role :app do
228
+ essentials "libfoo"
229
+ ruby version: "2.1.1"
230
+ nginx
231
+ end
232
+
233
+ role :db do
234
+ essentials
235
+ postgres
236
+ end
237
+
238
+ server "myapp.com", user: "deploy", roles: :app
239
+ server "staging.myapp.com", user: "deploy", roles: :app
240
+ server "db.myapp.com", user: "deploy", roles: :db
241
+ server "standby.myapp.com", user: "deploy", roles: [:app, :db]
242
+ end
243
+
244
+ Jisota.run(config)
245
+
246
+ ## Contributing
247
+
248
+ Any pull requests, suggestions, bug reports and feedback are most welcome :)
249
+
250
+ 1. Fork it ( http://github.com/lasseebert/jisota/fork )
251
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
252
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
253
+ 4. Push to the branch (`git push origin my-new-feature`)
254
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/jisota.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jisota/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jisota"
8
+ spec.version = Jisota::VERSION
9
+ spec.authors = ["Lasse Skindstad Ebert"]
10
+ spec.email = ["lasseebert@gmail.com"]
11
+ spec.summary = %q{Easily provision servers}
12
+ spec.description = %q{Easily provision servers using ruby, existing packages, your own packages and a nice DSL}
13
+ spec.homepage = "https://github.com/lasseebert/jisota"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "net-ssh", "~> 2.8.0"
22
+ spec.add_dependency "net-scp", "~> 1.1.2"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "simplecov"
27
+ spec.add_development_dependency "byebug"
28
+ spec.add_development_dependency "rspec", "~> 3.0.0.beta2"
29
+ spec.add_development_dependency "guard-rspec", "~> 4.2.8"
30
+ end
@@ -0,0 +1,52 @@
1
+ module Jisota
2
+
3
+ ##
4
+ # Stores items with a `:key` attribute
5
+ # In Jisota, this is used for collections of `Package`, `Role` and `Server`
6
+ #
7
+ # Unlike most ruby enumerables, errors are raised when adding duplicate item
8
+ # or getting non-existent item.
9
+ class Collection
10
+ extend Forwardable
11
+ include Enumerable
12
+
13
+ class KeyNotFoundError < StandardError; end
14
+ class DuplicateKeyError < StandardError; end
15
+
16
+ attr_reader :items
17
+
18
+ def_delegators :items, :size, :each, :has_key?
19
+
20
+ def initialize(items = {})
21
+ @items = items
22
+ end
23
+
24
+ def add(item_with_key)
25
+ key = item_with_key.key
26
+ raise DuplicateKeyError, "Collection already contains an item with key #{key.inspect}" if items.has_key?(key)
27
+ items[key] = item_with_key
28
+ end
29
+ alias_method :<<, :add
30
+
31
+ def [](key)
32
+ items.fetch(key) { raise KeyNotFoundError, "The key #{key.inspect} was not found in the collection" }
33
+ end
34
+
35
+ def first
36
+ @items.first[1]
37
+ end
38
+
39
+ def last
40
+ @items.last[1]
41
+ end
42
+
43
+ def each(&block)
44
+ @items.values.each(&block)
45
+ end
46
+
47
+ def merge(other, &block)
48
+ Collection.new(items.merge(other.items, &block))
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ module Jisota
2
+ ##
3
+ # Part of the Script duck type
4
+ #
5
+ # Stores a single shell command that can be executed
6
+ class CommandScript
7
+ attr_accessor :command
8
+
9
+ def initialize(command)
10
+ @command = command
11
+ end
12
+
13
+ def execute(ssh_session, logger = nil)
14
+ ssh_session.command(command, logger)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ module Jisota
2
+ ##
3
+ # Part of the Script duck type
4
+ #
5
+ # Contains a list of Scripts and can execute them consectutive.
6
+ # If one script fails, the following will not be executed and the combined
7
+ # result of this script will also be failed (`false`)
8
+ class CompositeScript
9
+ attr_accessor :scripts
10
+
11
+ def initialize
12
+ @scripts = []
13
+ end
14
+
15
+ def execute(ssh_session, logger = nil)
16
+ scripts.each do |inner|
17
+ result = inner.execute(ssh_session, logger)
18
+ return false unless result
19
+ end
20
+ true
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ module Jisota
2
+ ##
3
+ # Stores all information required to run a provision against one or multiple
4
+ # servers (Except for the global package manager, which is stored in the Jisota module).
5
+ #
6
+ # Uses DSL methods in the initializer for convenient definition of packages, roles and servers
7
+ class Configuration
8
+ attr_accessor :packages, :roles, :servers, :ssh_engine
9
+
10
+ def initialize(options = {}, &block)
11
+ @packages = options.fetch(:packages) { Collection.new }
12
+ @roles = options.fetch(:role) { Collection.new }
13
+ @servers = options.fetch(:servers) { [] }
14
+ @ssh_engine = options.fetch(:ssh_engine) { SSHEngine }
15
+ DSL.new(self).instance_eval(&block) if block_given?
16
+ end
17
+
18
+ def add_package(package)
19
+ packages.add(package)
20
+ end
21
+
22
+ def get_package(name)
23
+ packages[name]
24
+ end
25
+
26
+ def add_role(role)
27
+ roles.add(role)
28
+ end
29
+
30
+ def get_role(name)
31
+ roles[name]
32
+ end
33
+
34
+ def add_server(server)
35
+ servers << server
36
+ end
37
+
38
+ def each_server(&block)
39
+ servers.each(&block)
40
+ end
41
+
42
+ class DSL
43
+ def initialize(target)
44
+ @target = target
45
+ end
46
+
47
+ def package(name, &block)
48
+ @target.packages << Package.new(name, &block)
49
+ end
50
+
51
+ def role(name, &block)
52
+ @target.roles << Role.new(name, &block)
53
+ end
54
+
55
+ def server(host, options = {})
56
+ @target.servers << Server.new(host, options)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module Jisota
2
+ class ParameterError < StandardError; end
3
+ end
@@ -0,0 +1,21 @@
1
+ module Jisota
2
+ ##
3
+ # Part of the Script duck type
4
+ #
5
+ # Uploads a file on execution
6
+ class FileScript
7
+ attr_accessor :file
8
+
9
+ def initialize(file)
10
+ @file = file
11
+ end
12
+
13
+ def execute(ssh_session, logger = nil)
14
+ logger.prefixed_message("File #{file.from} -> #{file.to}") if logger
15
+ logger.indent if logger
16
+ result = ssh_session.upload(file, logger)
17
+ logger.outdent if logger
18
+ result
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ module Jisota
2
+ ##
3
+ # Responsible for outputting to the user / log file / whatever
4
+ #
5
+ # Will default to use STDOUT and STDERR, but that can be overwritten in the
6
+ # initializer
7
+ class Logger
8
+ attr_accessor :stdout, :stderr, :verbose, :prefix, :indent_level
9
+
10
+ def initialize(options = {})
11
+ @stdout = options.fetch(:stdout) { $stdout }
12
+ @stderr = options.fetch(:stderr) { $stderr }
13
+ @verbose = options.fetch(:verbose) { false }
14
+ @prefix = options.fetch(:prefix) { "-----> " }
15
+ @indent_level = 0
16
+ end
17
+
18
+ def command(command)
19
+ prefixed_message "Executing #{command}"
20
+ end
21
+
22
+ def upload(from: , to: )
23
+ prefixed_message "Uploading #{from} -> #{to}"
24
+ end
25
+
26
+ def package(package_script)
27
+ prefixed_message "Package #{package_script}"
28
+ end
29
+
30
+ def package_cancelled_by_verify(package_script)
31
+ prefixed_message "Package #{package_script} already installed"
32
+ end
33
+
34
+ def prefixed_message(message)
35
+ stdout.write(create_message(message, true, true))
36
+ end
37
+
38
+ def info(message)
39
+ stdout.write(message) if verbose
40
+ end
41
+
42
+ def warn(message)
43
+ stderr.write(message) if verbose
44
+ end
45
+
46
+ def error(message)
47
+ stderr.write(message)
48
+ end
49
+
50
+ def indent
51
+ self.indent_level += 1
52
+ end
53
+
54
+ def outdent
55
+ self.indent_level -= 1
56
+ end
57
+
58
+ private
59
+
60
+ def create_message(message, newline, use_prefix)
61
+ result = ""
62
+ if use_prefix
63
+ result << prefix
64
+ else
65
+ result << " " * prefix.size
66
+ end
67
+ result << " " * indent_level
68
+ result << message
69
+ result << "\n" if newline
70
+ result
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,35 @@
1
+ module Jisota
2
+ class Package
3
+ attr_accessor :name, :description, :params, :run_block, :verify_block
4
+
5
+ def initialize(name = nil, &block)
6
+ @params = []
7
+ @name = name
8
+ DSL.new(self).instance_eval(&block) if block_given?
9
+ end
10
+
11
+ alias_method :key, :name
12
+
13
+ class DSL
14
+ def initialize(target)
15
+ @target = target
16
+ end
17
+
18
+ def description(value)
19
+ @target.description = value
20
+ end
21
+
22
+ def param(name, options = {})
23
+ @target.params << Param.new(name, options)
24
+ end
25
+
26
+ def run(&block)
27
+ @target.run_block = ScriptBlock.new(&block)
28
+ end
29
+
30
+ def verify(&block)
31
+ @target.verify_block = ScriptBlock.new(&block)
32
+ end
33
+ end
34
+ end
35
+ end