jisota 0.0.1

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