sambot 0.1.69 → 0.1.83

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sambot/cli.rb +16 -18
  3. data/lib/sambot/commands/cookbook.rb +56 -38
  4. data/lib/sambot/commands/packer.rb +21 -0
  5. data/lib/sambot/commands/session.rb +15 -5
  6. data/lib/sambot/commands/workstation.rb +8 -10
  7. data/lib/sambot/domain/bastion_host.rb +59 -0
  8. data/lib/sambot/domain/chef/kitchen.rb +39 -0
  9. data/lib/sambot/domain/{cookbooks → chef}/metadata.rb +6 -5
  10. data/lib/sambot/domain/common/{application_exception.rb → application_error.rb} +1 -1
  11. data/lib/sambot/domain/common/config.rb +13 -6
  12. data/lib/sambot/domain/common/file_checker.rb +3 -2
  13. data/lib/sambot/domain/common/runtime.rb +5 -5
  14. data/lib/sambot/domain/common/template_provider.rb +1 -1
  15. data/lib/sambot/domain/cookbook.rb +103 -0
  16. data/lib/sambot/domain/dns.rb +24 -0
  17. data/lib/sambot/domain/packer.rb +26 -0
  18. data/lib/sambot/domain/session.rb +25 -0
  19. data/lib/sambot/domain/{workstations/ssh_config_file.rb → ssh/config_file.rb} +7 -7
  20. data/lib/sambot/domain/{workstations/ssh_config_section.rb → ssh/config_section.rb} +2 -2
  21. data/lib/sambot/domain/{workstations/ssh_parser.rb → ssh/parser.rb} +8 -7
  22. data/lib/sambot/domain/ui.rb +19 -0
  23. data/lib/sambot/domain/vault.rb +32 -0
  24. data/lib/sambot/domain/workstation.rb +25 -0
  25. data/lib/sambot/templates/{.kitchen.gcp.windows.yml → .kitchen.gcp.yml.erb} +33 -5
  26. data/lib/sambot/templates/.kitchen.rackspace.yml.erb +49 -0
  27. data/lib/sambot/templates/{.kitchen.centos.yml → .kitchen.yml.erb} +6 -1
  28. data/lib/sambot/templates/metadata.rb.erb +9 -2
  29. data/lib/sambot/templates/packer.linux.json +22 -0
  30. data/lib/sambot/templates/packer.windows.json.erb +18 -0
  31. data/lib/sambot/templates/teamcity.sh.erb +7 -7
  32. data/lib/sambot/version.rb +1 -1
  33. data/sambot.gemspec +7 -1
  34. metadata +120 -36
  35. data/lib/sambot/commands/secret.rb +0 -32
  36. data/lib/sambot/commands/teamcity.rb +0 -15
  37. data/lib/sambot/domain/common/ui.rb +0 -21
  38. data/lib/sambot/domain/cookbooks/assistant_chef.rb +0 -103
  39. data/lib/sambot/domain/cookbooks/kitchen.rb +0 -30
  40. data/lib/sambot/domain/secrets/vault.rb +0 -28
  41. data/lib/sambot/domain/workstations/env.rb +0 -0
  42. data/lib/sambot/domain/workstations/hosts.rb +0 -0
  43. data/lib/sambot/domain/workstations/install.sh +0 -1
  44. data/lib/sambot/templates/.kitchen.gcp.centos.yml +0 -39
  45. data/lib/sambot/templates/.kitchen.rackspace.centos.yml +0 -27
  46. data/lib/sambot/templates/.kitchen.rackspace.windows.yml +0 -34
  47. data/lib/sambot/templates/.kitchen.windows.yml +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 51c431904343df42faaf16c519b84a78274d0523
4
- data.tar.gz: 0a49af56edb773ae663c2a9f868270b75f8064bf
3
+ metadata.gz: a37d00724675ee46fdcf68b9ee7cdb2d1382d954
4
+ data.tar.gz: f5109307d67e54161b76ae51ee00e8ad4efe767c
5
5
  SHA512:
6
- metadata.gz: 93ae2180f67a75a58e13534dceef64499a8bf4912e367e80b5e406caf4f9276e610918dab9075a93e31cd840d8533a5e8d9911c85bec84e951842ed5686a979e
7
- data.tar.gz: 848bc4ae662bb690e05ffdfff4999a81bad9984d16db873bf9d3f464c44aa21619fc5973e93d79b6bcd0acf392c7f59efffcc0b12c5b3a2a1a6ef4d7987889b5
6
+ metadata.gz: 76dcfeb1c4f34cd18c03483011b78eb23dd015d18529227f6f24e0a50dfc23bfb7bc488cb0c03a02024470f4c209c0a1ef3d33341f13a62e00d15ea130202328
7
+ data.tar.gz: '08bfa75fda01edd760e02431c29fd960df0e0fc0de7b8645fe0c69949f08e5fa0db11ea39e958ec6af92751b8ed4651beef67319cf0235022ed4dc68f9997e67'
data/lib/sambot/cli.rb CHANGED
@@ -1,35 +1,33 @@
1
1
  require 'thor'
2
- require_relative 'domain/common/application_exception'
2
+ require_relative 'domain/common/application_error'
3
3
  require_relative 'domain/common/file_checker'
4
- require_relative 'domain/secrets/vault'
5
- require_relative 'domain/workstations/hosts'
6
- require_relative 'domain/workstations/env'
7
- require_relative 'domain/workstations/ssh_config_file'
8
- require_relative 'domain/workstations/ssh_config_section'
9
- require_relative 'domain/workstations/ssh_parser'
10
- require_relative 'domain/common/ui'
11
4
  require_relative 'domain/common/config'
12
5
  require_relative 'domain/common/runtime'
13
6
  require_relative 'domain/common/template_provider'
14
- require_relative 'domain/cookbooks/kitchen'
15
- require_relative 'domain/cookbooks/assistant_chef'
16
- require_relative 'domain/cookbooks/metadata'
7
+ require_relative 'domain/chef/kitchen'
8
+ require_relative 'domain/chef/metadata'
9
+ require_relative 'domain/ssh/config_file'
10
+ require_relative 'domain/ssh/config_section'
11
+ require_relative 'domain/ssh/parser'
12
+ require_relative 'domain/bastion_host'
13
+ require_relative 'domain/dns'
14
+ require_relative 'domain/session'
15
+ require_relative 'domain/vault'
16
+ require_relative 'domain/ui'
17
+ require_relative 'domain/cookbook'
18
+ require_relative 'domain/workstation'
17
19
  require_relative 'commands/cookbook'
18
- require_relative 'commands/secret'
19
- require_relative 'commands/teamcity'
20
+ require_relative 'commands/session'
20
21
  require_relative 'commands/workstation'
21
22
 
22
23
  module Sambot
23
24
  class CLI < Thor
24
25
 
25
- desc 'teamcity', 'Manage TeamCity'
26
- subcommand 'teamcity', Sambot::Commands::TeamCity
27
-
28
26
  desc 'cookbook', 'Manage Chef cookbooks'
29
27
  subcommand 'cookbook', Sambot::Commands::Cookbook
30
28
 
31
- desc 'secret', 'Manage secrets inside Chef cookbooks'
32
- subcommand 'secret', Sambot::Commands::Secret
29
+ desc 'session', 'Manage sessions'
30
+ subcommand 'session', Sambot::Commands::Session
33
31
 
34
32
  desc 'workstation', 'Manage engineer workstations'
35
33
  subcommand 'workstation', Sambot::Commands::Workstation
@@ -6,64 +6,82 @@ module Sambot
6
6
 
7
7
  class Cookbook < Thor
8
8
 
9
- include Domain::Common::UI
10
- include Domain::Common::Runtime
9
+ Runtime = Sambot::Domain::Common::Runtime
10
+ ApplicationError = Sambot::Domain::Common::ApplicationError
11
+
12
+ GUIDE =
13
+ {
14
+ clean: {
15
+ SHORT_DESC: 'Remove all generated build files from a cookbook',
16
+ LONG_DESC: <<-LONGDESC
17
+ `sambot cookbook clean` will remove all files generated by `sambot cookbook build`.
18
+ This is required to ensure the generated files are not stored in source control.
19
+ LONGDESC
20
+ },
21
+ build: {
22
+ SHORT_DESC: 'Builds a cookbook from its configuration file',
23
+ LONG_DESC: <<-LONGDESC
24
+ `sambot cookbook build` will generate all the files required for the functioning
25
+ of a Chef cookbook from a configuration file. The motivation behind this setup is to
26
+ standardize and simplify the Chef cookbook creation workflow. The configuration file should be
27
+ called .config.yml and stored in the root of the Chef cookbook.
28
+ LONGDESC
29
+ },
30
+ generate: {
31
+ SHORT_DESC: 'Creates a new cookbook',
32
+ LONG_DESC: <<-LONGDESC
33
+ `sambot cookbook generate` will create a new cookbook. This can be either a
34
+ wrapper cookbook or an instance role cookbook.
35
+ LONGDESC
36
+ },
37
+ version: {
38
+ SHORT_DESC: 'Gives the cookbook version as a TeamCity service message',
39
+ LONG_DESC: ''
40
+ }
41
+ }
11
42
 
12
43
  namespace 'cookbook'
13
44
 
14
- desc 'clean', 'Remove all generated build files from a cookbook'
15
- long_desc <<-LONGDESC
16
- `sambot cookbook clean` will remove all files generated by `sambot cookbook build`.
17
- This is required to ensure the generated files are not stored in source control.
18
- LONGDESC
45
+ desc 'clean', GUIDE[:clean][:SHORT_DESC]
46
+ long_desc GUIDE[:clean][:LONG_DESC]
19
47
  def clean
20
- ensure_latest
21
- info('Removing all generated files from this cookbook.')
22
- modified_files = Sambot::Domain::Cookbooks::AssistantChef.new.clean_cookbook(GENERATED_FILES - ['.gitignore'])
23
- modified_files.each {|file| debug("./#{file} has been removed.")}
24
- info('The cookbook has been successfully cleaned.')
25
- rescue Domain::Common::ApplicationException => e
48
+ Runtime.ensure_latest
49
+ Domain::Cookbook.clean(GENERATED_FILES - ['.gitignore'])
50
+ rescue ApplicationError => e
26
51
  error(e.message)
27
52
  end
28
53
 
29
- desc 'build', 'Builds a cookbook from its configuration file'
30
- long_desc <<-LONGDESC
31
- `sambot cookbook build` will generate all the files required for the functioning
32
- of a Chef cookbook from a configuration file. The motivation behind this setup is to
33
- standardize and simplify the Chef cookbook creation workflow. The configuration file should be
34
- called .config.yml and stored in the root of the Chef cookbook.
35
- LONGDESC
54
+ desc 'build', GUIDE[:clean][:SHORT_DESC]
55
+ long_desc GUIDE[:clean][:LONG_DESC]
36
56
  def build
37
- ensure_latest
38
- modified_files = Domain::Cookbooks::AssistantChef.new.build_cookbook(ESSENTIAL_FILES, GENERATED_FILES)
39
- modified_files.each {|file| debug("./#{file} has been added to the cookbook.")}
40
- info('The cookbook has been successfully built.')
41
- rescue Domain::Common::ApplicationException => e
57
+ Runtime.ensure_latest
58
+ Domain::Cookbook.build(ESSENTIAL_FILES, GENERATED_FILES)
59
+ rescue ApplicationError => e
42
60
  error(e.message)
43
61
  end
44
62
 
45
- desc 'generate', 'Creates a new cookbook'
46
- long_desc <<-LONGDESC
47
- `sambot cookbook generate` will create a new cookbook. This can be either a
48
- wrapper cookbook or an instance role cookbook.
49
- LONGDESC
63
+ desc 'generate', GUIDE[:generate][:SHORT_DESC]
64
+ long_desc GUIDE[:generate][:LONG_DESC]
50
65
  def generate()
51
- ensure_latest
66
+ Runtime.ensure_latest
52
67
  name = ask(' What is the name of this cookbook?')
53
68
  description = ask(' What does this cookbook do?')
54
- platform = ask(' What operating system will this cookbook run on?', :limited_to => ['windows', 'linux'])
69
+ platforms = ask(' What operating system will this cookbook run on?', :limited_to => ['windows', 'centos', 'both'])
55
70
  type = ask(' What type of cookbook will this be?', :limited_to => ['wrapper', 'role'])
56
- Sambot::Domain::Cookbooks::AssistantChef.new.generate_cookbook(name, platform, type, description, ESSENTIAL_FILES, GENERATED_FILES)
57
- info('The cookbook has been successfully generated.')
58
- rescue Domain::Common::ApplicationException => e
71
+ platforms = platforms == 'both' ? ['centos', 'windows'] : [platforms]
72
+ Domain::Cookbook.generate(name, platforms, type, description, ESSENTIAL_FILES, GENERATED_FILES)
73
+ rescue ApplicationError => e
59
74
  error(e.message)
60
75
  end
61
76
 
62
- desc 'version', 'Gives the cookbook version as a TeamCity service message'
77
+ desc 'version', GUIDE[:version][:SHORT_DESC]
78
+ long_desc GUIDE[:version][:LONG_DESC]
63
79
  def version()
64
- result = Domain::Common::Config.new.read
65
- puts result.inspect
80
+ Runtime.ensure_latest
81
+ result = Domain::Common::Config.read
66
82
  puts "##teamcity[buildNumber '#{result['version'].to_s}']"
83
+ rescue ApplicationError => e
84
+ error(e.message)
67
85
  end
68
86
 
69
87
  end
@@ -0,0 +1,21 @@
1
+ module Sambot
2
+ module Commands
3
+
4
+ class Packer < Thor
5
+
6
+ ApplicationError = Sambot::Domain::Common::ApplicationError
7
+ Runtime = Sambot::Domain::Common::Runtime
8
+
9
+ namespace 'packer'
10
+
11
+ desc "prepare", "Sets up a Packer configuration"
12
+ def prepare
13
+ Runtime.ensure_latest
14
+ Domain::Packer.prepare
15
+ rescue ApplicationError => e
16
+ error(e.message)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -3,18 +3,28 @@ module Sambot
3
3
 
4
4
  class Session < Thor
5
5
 
6
+ ApplicationError = Sambot::Domain::Common::ApplicationError
7
+ Runtime = Sambot::Domain::Common::Runtime
8
+
6
9
  namespace 'session'
7
10
 
8
11
  desc "start", "Start a new DEV/QE session"
9
12
  def start
10
- username = ask('What is your DEV/QE username?')
11
- password = ask('What is your DEV/QE password?')
12
- puts "not implemented"
13
+ Runtime.ensure_latest
14
+ username = ask(' What is your DEV/QE username i.e. jsmith? ')
15
+ password = ask(' What is your DEV/QE password? ', :echo => false)
16
+ say('')
17
+ Domain::Session.start(username, password)
18
+ rescue ApplicationError => e
19
+ error(e.message)
13
20
  end
14
21
 
15
22
  desc "stop", "Stop the DEV/QE session"
16
- def start
17
- puts "not implemented"
23
+ def stop
24
+ Runtime.ensure_latest
25
+ Domain::Session.stop()
26
+ rescue ApplicationError => e
27
+ error(e.message)
18
28
  end
19
29
 
20
30
  end
@@ -3,19 +3,17 @@ module Sambot
3
3
 
4
4
  class Workstation < Thor
5
5
 
6
+ Runtime = Sambot::Domain::Common::Runtime
7
+
6
8
  namespace 'workstation'
7
9
 
8
- desc "cnfigure", "Sets up an engineering workstation"
10
+ desc "configure", "Sets up an engineering workstation"
9
11
  def configure
10
- username = ask("What is your DEV/QE Active Directory username i.e. john.smith?")
11
- debug("Updating your SSH configuration.")
12
- config = Domain::Workstations::SshParser.new.update('DEV\\' + username)
13
- config.save
14
- debug("Updating your environment variables.")
15
- Domain::Workstations::Env.new.update
16
- debug("Generating the datasource for Free Remote Desktop Manager, your RDP client.")
17
- filename = Domain::Workstations::Hosts.new.generate
18
- info("Your workstation is now ready for use. The RDM datasource is available here: #{filename}")
12
+ Runtime.ensure_latest
13
+ username = ask(" What is your DEV/QE Active Directory username i.e. john.smith? ")
14
+ Domain::Workstation.configure(username)
15
+ rescue ApplicationError => e
16
+ error(e.message)
19
17
  end
20
18
 
21
19
  end
@@ -0,0 +1,59 @@
1
+ require 'awesome_print'
2
+ require 'process_exists'
3
+
4
+ module Sambot
5
+ module Domain
6
+ class BastionHost
7
+
8
+ ApplicationError = Sambot::Domain::Common::ApplicationError
9
+ Parser = Sambot::Domain::Ssh::Parser
10
+
11
+ def self.connect(username, password)
12
+ disconnect
13
+ cmd = "sshpass -p #{password} ssh -N -f -l DEV\\\\#{username} bastion "
14
+ success = system(cmd)
15
+ sleep(3)
16
+ unless success
17
+ raise ApplicationError.new('Unable to open SSH tunnels to the bastion host')
18
+ end
19
+ self
20
+ end
21
+
22
+ def self.forwards(config_file = nil)
23
+ config = ConfigFile.new(config_file)
24
+ []
25
+ end
26
+
27
+ def self.disconnect()
28
+ close_tunnels(list_tunnels)
29
+ end
30
+
31
+ def self.close_tunnels(tunnels)
32
+ if tunnels
33
+ tunnels.each do |tunnel|
34
+ components = tunnel.split(' ')
35
+ pid = components[1].to_i
36
+ parse_forwards.each do |forward|
37
+ if components[8] == forward && components[9] == '(LISTEN)' && Process.exists?(pid)
38
+ Process.kill('INT', pid)
39
+ UI.debug("The process #{pid}, parent of the tunnel #{forward}, has been killed.")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.parse_forwards
47
+ Parser::ENTRIES[:bastion][:LocalForward].map do |entry|
48
+ "127.0.0.1:#{entry.split(' ')[0]}"
49
+ end
50
+ end
51
+
52
+ def self.list_tunnels
53
+ result = `lsof -i -n -P | grep -E '^ssh'`
54
+ result.split(/\n/)
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ module Sambot
2
+ module Domain
3
+ module Chef
4
+ class Kitchen
5
+
6
+ KITCHEN_LOCAL_YML = ".kitchen.yml"
7
+ KITCHEN_GCP_YML = ".kitchen.gcp.yml"
8
+ KITCHEN_RACKSPACE_YML = ".kitchen.rackspace.yml"
9
+
10
+
11
+ def self.generate_yml(cookbook_name, platforms, suites = nil)
12
+ raise ApplicationError, 'Missing platforms when trying to generate Test-Kitchen YAML.' unless platforms
13
+ raise ApplicationError, 'Missing cookbook name when trying to generate Test-Kitchen YAML.' unless cookbook_name
14
+ test_kitchen_configs = {
15
+ "#{KITCHEN_LOCAL_YML}": read_template(KITCHEN_LOCAL_YML, cookbook_name, platforms),
16
+ "#{KITCHEN_GCP_YML}": read_template(KITCHEN_GCP_YML, cookbook_name, platforms),
17
+ "#{KITCHEN_RACKSPACE_YML}": read_template(KITCHEN_RACKSPACE_YML, cookbook_name, platforms)
18
+ }
19
+ test_kitchen_configs.each do |key, value|
20
+ value['suites'] = suites if suites
21
+ test_kitchen_configs[key] = value.to_yaml
22
+ end
23
+ test_kitchen_configs
24
+ end
25
+
26
+ private
27
+
28
+ def self.read_template(template, cookbook_name, platforms)
29
+ filename = File.join(TemplateProvider.get_path("#{template}.erb"))
30
+ contents = File.read(filename).gsub(/@@cookbook_name@@/, cookbook_name)
31
+ eruby = Erubis::Eruby.new(contents)
32
+ yaml = eruby.evaluate({platforms: platforms}).gsub(/\_@/, "<%=").gsub(/@\_/, "%>")
33
+ YAML.load(yaml)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,23 +2,24 @@ require 'erubis'
2
2
 
3
3
  module Sambot
4
4
  module Domain
5
- module Cookbooks
5
+ module Chef
6
6
  class Metadata
7
7
 
8
- def generate(name, platform, version, description, dependencies = nil)
8
+ def self.generate(name, platforms, version, description, dependencies = nil, gems = nil)
9
9
  context = {
10
10
  'cookbook_name' => name,
11
- 'cookbook_platform' => platform,
11
+ 'cookbook_platforms' => platforms,
12
12
  'cookbook_version' => version,
13
13
  'cookbook_description' => description,
14
- 'cookbook_dependencies' => dependencies
14
+ 'cookbook_dependencies' => dependencies,
15
+ 'cookbook_gems' => gems,
15
16
  }
16
17
  generate_metadata(context)
17
18
  end
18
19
 
19
20
  private
20
21
 
21
- def generate_metadata(context)
22
+ def self.generate_metadata(context)
22
23
  filename = File.join(File.dirname(__FILE__), '../../templates', 'metadata.rb.erb')
23
24
  input = File.read(filename)
24
25
  eruby = Erubis::Eruby.new(input)
@@ -1,7 +1,7 @@
1
1
  module Sambot
2
2
  module Domain
3
3
  module Common
4
- class ApplicationException < RuntimeError
4
+ class ApplicationError < RuntimeError
5
5
 
6
6
  end
7
7
  end
@@ -9,13 +9,20 @@ module Sambot
9
9
 
10
10
  def read(path = nil)
11
11
  path ||= File.join(Dir.getwd, CONFIGURATION_FILENAME)
12
- raise ApplicationException, "The configuration file was not found at #{path}." unless File.exist?(path)
12
+ raise ApplicationError, "The configuration file was not found at #{path}." unless File.exist?(path)
13
13
  config = YAML.load_file(path)
14
- raise ApplicationException, 'Missing cookbook name in the .config.yml configuration file' unless config['name']
15
- raise ApplicationException, 'Missing platform in the .config.yml configuration file' unless config['platform']
16
- raise ApplicationException, 'Missing version in the .config.yml configuration file' unless config['version']
17
- raise ApplicationException, 'Missing list of suites in the .config.yml configuration file' unless config['suites']
18
- raise ApplicationException, 'Missing description in the .config.yml configuration file' unless config['description']
14
+ raise ApplicationError, 'Missing cookbook name in the .config.yml configuration file' unless config['name']
15
+ raise ApplicationError, 'Missing platforms in the .config.yml configuration file' unless config['platform'] || config['platforms']
16
+ raise ApplicationError, 'Missing version in the .config.yml configuration file' unless config['version']
17
+ raise ApplicationError, 'Missing list of suites in the .config.yml configuration file' unless config['suites']
18
+ raise ApplicationError, 'Missing description in the .config.yml configuration file' unless config['description']
19
+ # Dealing with legacy tech debt of allowing multiple platforms rather than a single platform
20
+ unless config['platforms']
21
+ unless config['platform'].kind_of?(Array)
22
+ config['platform'] = [config['platform']]
23
+ end
24
+ config['platforms'] = config['platform']
25
+ end
19
26
  config
20
27
  end
21
28
 
@@ -5,7 +5,7 @@ module Sambot
5
5
 
6
6
  def verify(path)
7
7
  return if File.exist?(path) || Dir.exist?(path)
8
- raise ApplicationException, "The file or directory #{path} was not found in the current directory."
8
+ raise ApplicationError, "The file or directory #{path} was not found in the current directory."
9
9
  end
10
10
 
11
11
  def update(files)
@@ -19,7 +19,7 @@ module Sambot
19
19
 
20
20
  def update_template(filename)
21
21
  working_path = filename.end_with?(".erb") ? filename.gsub(/\.erb/, '') : filename
22
- template_path = TemplateProvider.new.get_path(filename)
22
+ template_path = TemplateProvider.get_path(filename)
23
23
  File.delete(working_path) if File.exist?(working_path)
24
24
  if template_path.end_with?(".erb")
25
25
  input = File.read(template_path)
@@ -34,6 +34,7 @@ module Sambot
34
34
  else
35
35
  FileUtils.cp(template_path, working_path)
36
36
  end
37
+ UI.debug("#{working_path} has been added to the cookbook.")
37
38
  end
38
39
 
39
40
  end