itamae 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +14 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +60 -0
  8. data/Rakefile +62 -0
  9. data/bin/itamae +5 -0
  10. data/example/foo +1 -0
  11. data/example/node.json +1 -0
  12. data/example/recipe.rb +15 -0
  13. data/itamae.gemspec +32 -0
  14. data/lib/itamae.rb +13 -0
  15. data/lib/itamae/cli.rb +23 -0
  16. data/lib/itamae/logger.rb +44 -0
  17. data/lib/itamae/node.rb +9 -0
  18. data/lib/itamae/recipe.rb +46 -0
  19. data/lib/itamae/resources.rb +25 -0
  20. data/lib/itamae/resources/base.rb +151 -0
  21. data/lib/itamae/resources/directory.rb +26 -0
  22. data/lib/itamae/resources/file.rb +36 -0
  23. data/lib/itamae/resources/package.rb +15 -0
  24. data/lib/itamae/resources/remote_file.rb +16 -0
  25. data/lib/itamae/resources/template.rb +19 -0
  26. data/lib/itamae/runner.rb +49 -0
  27. data/lib/itamae/specinfra.rb +44 -0
  28. data/lib/itamae/version.rb +3 -0
  29. data/spec/integration/Vagrantfile +19 -0
  30. data/spec/integration/default_spec.rb +33 -0
  31. data/spec/integration/recipes/default.rb +26 -0
  32. data/spec/integration/recipes/hello.erb +1 -0
  33. data/spec/integration/recipes/hello.txt +1 -0
  34. data/spec/integration/recipes/node.json +3 -0
  35. data/spec/integration/spec_helper.rb +45 -0
  36. data/spec/unit/lib/itamae/logger_spec.rb +20 -0
  37. data/spec/unit/lib/itamae/node_spec.rb +6 -0
  38. data/spec/unit/lib/itamae/recipe_spec.rb +6 -0
  39. data/spec/unit/lib/itamae/resources/base_spec.rb +135 -0
  40. data/spec/unit/lib/itamae/resources/package_spec.rb +18 -0
  41. data/spec/unit/lib/itamae/resources/remote_file_spec.rb +23 -0
  42. data/spec/unit/lib/itamae/resources_spec.rb +14 -0
  43. data/spec/unit/lib/itamae/runner_spec.rb +29 -0
  44. data/spec/unit/spec_helper.rb +22 -0
  45. metadata +230 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e8b0f4978f8d00ca41234de5b288befcdc1f0f68
4
+ data.tar.gz: 20ad47b014f03d7af8ac547152281c5c0fbd9f99
5
+ SHA512:
6
+ metadata.gz: dd694c3e13fa40bbee8862338c59ff8079dd30763ba2710c9868e9a4dda8bda080baef7a46a8a21c4b67fdecab051447eb2064015625e1e5f843f741de767c85
7
+ data.tar.gz: 3d95d7a8a67337c18b56d569e3b6e45dfe65331e9a522a5d7f5a8586c330698c81199699584f94080b52befdaf561ffd5d4c9b683958244f1175515e8abf735f
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
+ .vagrant
19
+ Gemfile.local
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -fd
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.0.0"
4
+ - "1.9.3"
5
+ script:
6
+ - bundle exec rake spec:unit
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in itamae.gemspec
4
+ gemspec
5
+
6
+ path = Pathname.new("Gemfile.local")
7
+ eval(path.read) if path.exist?
8
+
9
+ group :test do
10
+ if RUBY_PLATFORM.include?('darwin')
11
+ gem 'growl'
12
+ end
13
+ end
14
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013-2014 Ryota Arai
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,60 @@
1
+ # Itamae
2
+
3
+ Configuration management tool like Chef which is simpler and lighter than Chef
4
+
5
+ ## Concept
6
+
7
+ * Good DSL like Chef
8
+ * Simpler and lighter than Chef
9
+ * It's just like Chef. No compatibility.
10
+ * Idempotent.
11
+
12
+ ## Installation
13
+
14
+ ```
15
+ $ gem install itamae
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Run locally
21
+
22
+ ```
23
+ $ sudo itamae execute -j example/node.json example/recipe.rb
24
+ D, [2013-12-24T14:05:50.859587 #7156] DEBUG -- : Loading node data from /vagrant/example/node.json ...
25
+ I, [2013-12-24T14:05:50.862072 #7156] INFO -- : >>> Executing Itamae::Resources::Package ({:action=>:install, :name=>"git"})...
26
+ D, [2013-12-24T14:05:51.335070 #7156] DEBUG -- : Command `apt-get -y install git` succeeded
27
+ D, [2013-12-24T14:05:51.335251 #7156] DEBUG -- : STDOUT> Reading package lists...
28
+ Building dependency tree...
29
+ Reading state information...
30
+ git is already the newest version.
31
+ 0 upgraded, 0 newly installed, 0 to remove and 156 not upgraded.
32
+ D, [2013-12-24T14:05:51.335464 #7156] DEBUG -- : STDERR>
33
+ I, [2013-12-24T14:05:51.335531 #7156] INFO -- : <<< Succeeded.
34
+ I, [2013-12-24T14:05:51.335728 #7156] INFO -- : >>> Executing Itamae::Resources::File ({:action=>:create, :source=>"foo", :path=>"/home/vagrant/foo"})...
35
+ D, [2013-12-24T14:05:51.335842 #7156] DEBUG -- : Copying a file from '/vagrant/example/foo' to '/home/vagrant/foo'...
36
+ I, [2013-12-24T14:05:51.339119 #7156] INFO -- : <<< Succeeded.
37
+ ```
38
+
39
+ ### Run via SSH
40
+
41
+ ```
42
+ $ itamae ssh -j example/node.json -h 192.168.10.10 -p 22 -u user -i /path/to/private_key example/recipe.rb
43
+ ```
44
+
45
+ ## Run tests
46
+
47
+ Requirements: Vagrant
48
+
49
+ ```
50
+ $ bundle exec rake spec
51
+ ```
52
+
53
+ ## Contributing
54
+
55
+ 1. Fork it
56
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
57
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
58
+ 4. Push to the branch (`git push origin my-new-feature`)
59
+ 5. Create new Pull Request
60
+
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ require 'tempfile'
4
+ require 'net/ssh'
5
+
6
+ desc 'Run unit and integration specs.'
7
+ task :spec => ['spec:unit', 'spec:integration:all']
8
+
9
+ namespace :spec do
10
+ RSpec::Core::RakeTask.new("unit") do |task|
11
+ task.ruby_opts = '-I ./spec/unit'
12
+ task.pattern = "./spec/unit{,/*/**}/*_spec.rb"
13
+ end
14
+
15
+ namespace :integration do
16
+ targets = []
17
+ Bundler.with_clean_env do
18
+ `cd spec/integration && /usr/bin/vagrant status`.split("\n\n")[1].each_line do |line|
19
+ targets << line.match(/^[^ ]+/)[0]
20
+ end
21
+ end
22
+
23
+ task :all => targets
24
+
25
+ targets.each do |target|
26
+ desc "Run provision and specs to #{target}"
27
+ task target => ["provision:#{target}", "serverspec:#{target}"]
28
+
29
+ namespace :provision do
30
+ task target do
31
+ Bundler.with_clean_env do
32
+ config = Tempfile.new('', Dir.tmpdir)
33
+ env = {"VAGRANT_CWD" => File.expand_path('./spec/integration')}
34
+ system env, "/usr/bin/vagrant up #{target}"
35
+ system env, "/usr/bin/vagrant ssh-config #{target} > #{config.path}"
36
+ options = Net::SSH::Config.for(target, [config.path])
37
+
38
+ cmd = "bundle exec bin/itamae ssh"
39
+ cmd << " -h #{options[:host_name]}"
40
+ cmd << " -u #{options[:user]}"
41
+ cmd << " -p #{options[:port]}"
42
+ cmd << " -i #{options[:keys].first}"
43
+ cmd << " -j spec/integration/recipes/node.json"
44
+ cmd << " spec/integration/recipes/default.rb"
45
+
46
+ system cmd
47
+ abort unless $?.exitstatus == 0
48
+ end
49
+ end
50
+ end
51
+
52
+ namespace :serverspec do
53
+ desc "Run serverspec tests to #{target}"
54
+ RSpec::Core::RakeTask.new(target.to_sym) do |t|
55
+ ENV['TARGET_HOST'] = target
56
+ t.ruby_opts = '-I ./spec/integration'
57
+ t.pattern = "spec/integration/*_spec.rb"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/bin/itamae ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'itamae/cli'
4
+ Itamae::CLI.start
5
+
data/example/foo ADDED
@@ -0,0 +1 @@
1
+ Hello
data/example/node.json ADDED
@@ -0,0 +1 @@
1
+ {"file_source": "foo"}
data/example/recipe.rb ADDED
@@ -0,0 +1,15 @@
1
+ package "git" do
2
+ action :install
3
+ end
4
+
5
+ remote_file '/home/vagrant/foo' do
6
+ source node['file_source']
7
+ end
8
+
9
+ directory '/tmp/itamae' do
10
+ action :create
11
+ mode '0777'
12
+ owner 'vagrant'
13
+ group 'vagrant'
14
+ end
15
+
data/itamae.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'itamae/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "itamae"
8
+ spec.version = Itamae::VERSION
9
+ spec.authors = ["Ryota Arai"]
10
+ spec.email = ["ryota.arai@gmail.com"]
11
+ spec.summary = %q{Simple Configuration Management Tool}
12
+ spec.homepage = "https://github.com/ryotarai/itamae"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency "thor"
21
+ spec.add_runtime_dependency "specinfra", "2.0.0.beta20"
22
+ spec.add_runtime_dependency "hashie"
23
+
24
+ # TODO: move to specinfra
25
+ spec.add_runtime_dependency "net-scp"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.3"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "rspec", "~> 3.0"
30
+ spec.add_development_dependency "serverspec", "2.0.0.beta16"
31
+ spec.add_development_dependency "pry-byebug"
32
+ end
data/lib/itamae.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "itamae/version"
2
+ require "itamae/runner"
3
+ require "itamae/cli"
4
+ require "itamae/recipe"
5
+ require "itamae/resources"
6
+ require "itamae/logger"
7
+ require "itamae/node"
8
+ require "itamae/specinfra"
9
+
10
+ module Itamae
11
+ # Your code goes here...
12
+ end
13
+
data/lib/itamae/cli.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'itamae'
2
+ require 'thor'
3
+
4
+ module Itamae
5
+ class CLI < Thor
6
+ desc "local RECIPE [RECIPE...]", "Run Itamae locally"
7
+ option :node_json, type: :string, aliases: ['-j']
8
+ def local(*recipe_files)
9
+ Runner.run(recipe_files, :local, options)
10
+ end
11
+
12
+ desc "ssh RECIPE [RECIPE...]", "Run Itamae via ssh"
13
+ option :node_json, type: :string, aliases: ['-j']
14
+ option :host, required: true, type: :string, aliases: ['-h']
15
+ option :user, type: :string, aliases: ['-u']
16
+ option :key, type: :string, aliases: ['-i']
17
+ option :port, type: :numeric, aliases: ['-p']
18
+ def ssh(*recipe_files)
19
+ Runner.run(recipe_files, :ssh, options)
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,44 @@
1
+ require 'itamae'
2
+ require 'logger'
3
+
4
+ module Itamae
5
+ module Logger
6
+ class Formatter
7
+ def call(severity, datetime, progname, msg)
8
+ "[%s] %5s : %s\n" % [format_datetime(datetime), severity, msg2str(msg)]
9
+ end
10
+
11
+ private
12
+ def format_datetime(time)
13
+ time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d" % time.usec
14
+ end
15
+
16
+ def msg2str(msg)
17
+ case msg
18
+ when ::String
19
+ msg
20
+ when ::Exception
21
+ "#{ msg.message } (#{ msg.class })\n" <<
22
+ (msg.backtrace || []).join("\n")
23
+ else
24
+ msg.inspect
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.logger
30
+ @logger ||= ::Logger.new($stdout).tap do |logger|
31
+ logger.formatter = Formatter.new
32
+ end
33
+ end
34
+
35
+ def self.logger=(l)
36
+ @logger = l
37
+ end
38
+
39
+ def self.method_missing(method, *args, &block)
40
+ logger.public_send(method, *args, &block)
41
+ end
42
+ end
43
+ end
44
+
@@ -0,0 +1,9 @@
1
+ require 'itamae'
2
+ require 'hashie'
3
+ require 'json'
4
+
5
+ module Itamae
6
+ class Node < Hashie::Mash
7
+ end
8
+ end
9
+
@@ -0,0 +1,46 @@
1
+ require 'itamae'
2
+
3
+ module Itamae
4
+ class Recipe
5
+ attr_reader :path
6
+ attr_reader :runner
7
+
8
+ def initialize(runner, path)
9
+ @runner = runner
10
+ @path = path
11
+ @resources = []
12
+ load_resources
13
+ end
14
+
15
+ def node
16
+ @runner.node
17
+ end
18
+
19
+ def run
20
+ @resources.each do |resource|
21
+ Logger.info ">>> Executing #{resource.class.name} (#{resource.options})..."
22
+ begin
23
+ resource.run
24
+ rescue Resources::CommandExecutionError
25
+ Logger.error "<<< Failed."
26
+ exit 2
27
+ else
28
+ Logger.info "<<< Succeeded."
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def load_resources
36
+ instance_eval(File.read(@path), @path, 1)
37
+ end
38
+
39
+ def method_missing(method, name, &block)
40
+ klass = Resources.get_resource_class(method)
41
+ resource = klass.new(self, name, &block)
42
+ @resources << resource
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,25 @@
1
+ require 'itamae'
2
+ require 'itamae/resources/base'
3
+ require 'itamae/resources/file'
4
+ require 'itamae/resources/package'
5
+ require 'itamae/resources/remote_file'
6
+ require 'itamae/resources/directory'
7
+ require 'itamae/resources/template'
8
+
9
+ module Itamae
10
+ module Resources
11
+ Error = Class.new(StandardError)
12
+ CommandExecutionError = Class.new(StandardError)
13
+ OptionMissingError = Class.new(StandardError)
14
+ InvalidTypeError = Class.new(StandardError)
15
+ NotSupportedOsError = Class.new(StandardError)
16
+
17
+ def self.get_resource_class_name(method)
18
+ method.to_s.split('_').map {|part| part.capitalize}.join
19
+ end
20
+
21
+ def self.get_resource_class(method)
22
+ const_get(get_resource_class_name(method))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,151 @@
1
+ require 'itamae'
2
+ require 'shellwords'
3
+
4
+ module Itamae
5
+ module Resources
6
+ class Base
7
+ @defined_options ||= {}
8
+ @supported_oses ||= []
9
+
10
+ class << self
11
+ attr_reader :defined_options
12
+ attr_reader :supported_oses
13
+
14
+ def inherited(subclass)
15
+ subclass.instance_variable_set(
16
+ :@defined_options,
17
+ self.defined_options.dup
18
+ )
19
+ end
20
+
21
+ def define_option(name, options)
22
+ current = @defined_options[name.to_sym] || {}
23
+ @defined_options[name.to_sym] = current.merge(options)
24
+ end
25
+
26
+ def support_os(hash)
27
+ @supported_oses << hash
28
+ end
29
+ end
30
+
31
+ define_option :action, type: Symbol, required: true
32
+
33
+ attr_reader :resource_name
34
+ attr_reader :options
35
+
36
+ def initialize(recipe, resource_name, &block)
37
+ @options = {}
38
+ @recipe = recipe
39
+ @resource_name = resource_name
40
+
41
+ instance_eval(&block) if block_given?
42
+
43
+ process_options
44
+ ensure_os
45
+ end
46
+
47
+ def run
48
+ public_send("#{action}_action".to_sym)
49
+ end
50
+
51
+ def nothing_action
52
+ # do nothing
53
+ end
54
+
55
+ private
56
+
57
+ def method_missing(method, *args)
58
+ if args.size == 1 && self.class.defined_options[method]
59
+ return @options[method] = args.first
60
+ elsif args.size == 0 && @options.has_key?(method)
61
+ return @options[method]
62
+ end
63
+ super
64
+ end
65
+
66
+ def process_options
67
+ self.class.defined_options.each_pair do |key, details|
68
+ @options[key] ||= @resource_name if details[:default_name]
69
+ @options[key] ||= details[:default]
70
+
71
+ if details[:required] && !@options[key]
72
+ raise Resources::OptionMissingError, "'#{key}' option is required but it is not set."
73
+ end
74
+
75
+ if @options[key] && details[:type] && !@options[key].is_a?(details[:type])
76
+ raise Resources::InvalidTypeError, "#{key} option should be #{details[:type]}."
77
+ end
78
+ end
79
+ end
80
+
81
+ def run_specinfra(type, *args)
82
+ command = Specinfra.command.public_send(type, *args)
83
+ run_command(command)
84
+ end
85
+
86
+ def run_command(command)
87
+ result = backend.run_command(command)
88
+ exit_status = result.exit_status
89
+
90
+ if exit_status == 0
91
+ method = :debug
92
+ Logger.public_send(method, "Command `#{command}` succeeded")
93
+ else
94
+ method = :error
95
+ Logger.public_send(method, "Command `#{command}` failed. (exit status: #{exit_status})")
96
+ end
97
+
98
+ if result.stdout && result.stdout != ''
99
+ Logger.public_send(method, "STDOUT> #{result.stdout.chomp}")
100
+ end
101
+ if result.stderr && result.stderr != ''
102
+ Logger.public_send(method, "STDERR> #{result.stderr.chomp}")
103
+ end
104
+
105
+ unless exit_status == 0
106
+ raise CommandExecutionError
107
+ end
108
+ end
109
+
110
+ def copy_file(src, dst)
111
+ Logger.debug "Copying a file from '#{src}' to '#{dst}'..."
112
+ unless ::File.exist?(src)
113
+ raise Error, "The file '#{src}' doesn't exist."
114
+ end
115
+ unless backend.copy_file(src, dst)
116
+ raise Error, "Copying a file failed."
117
+ end
118
+ end
119
+
120
+ def node
121
+ runner.node
122
+ end
123
+
124
+ def backend
125
+ Itamae.backend
126
+ end
127
+
128
+ def runner
129
+ @recipe.runner
130
+ end
131
+
132
+ def shell_escape(str)
133
+ Shellwords.escape(str)
134
+ end
135
+
136
+ def ensure_os
137
+ return unless self.class.supported_oses
138
+ ok = self.class.supported_oses.any? do |supported|
139
+ supported.each_pair.all? do |k, v|
140
+ backend.os[k] == v
141
+ end
142
+ end
143
+
144
+ unless ok
145
+ raise NotSupportedOsError, "#{self.class.name} resource doesn't support this OS now."
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+