knife-google 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.gitignore +22 -0
  2. data/CONTRIB.md +64 -0
  3. data/Gemfile +11 -0
  4. data/README.md +287 -0
  5. data/Rakefile +53 -0
  6. data/knife-google.gemspec +26 -0
  7. data/lib/chef/knife/google_base.rb +39 -68
  8. data/lib/chef/knife/google_disk_create.rb +60 -0
  9. data/lib/chef/knife/google_disk_delete.rb +60 -0
  10. data/lib/chef/knife/google_disk_list.rb +75 -0
  11. data/lib/chef/knife/google_server_create.rb +273 -184
  12. data/lib/chef/knife/google_server_delete.rb +74 -32
  13. data/lib/chef/knife/google_server_list.rb +45 -64
  14. data/lib/chef/knife/google_setup.rb +31 -0
  15. data/lib/chef/knife/google_zone_list.rb +78 -0
  16. data/lib/google/compute.rb +46 -0
  17. data/lib/google/compute/client.rb +188 -0
  18. data/lib/google/compute/config.rb +23 -0
  19. data/lib/google/compute/creatable_resource_collection.rb +38 -0
  20. data/lib/google/compute/deletable_resource_collection.rb +51 -0
  21. data/lib/google/compute/disk.rb +40 -0
  22. data/lib/google/compute/exception.rb +28 -0
  23. data/lib/google/compute/firewall.rb +65 -0
  24. data/lib/google/compute/global_operation.rb +60 -0
  25. data/lib/google/compute/image.rb +30 -0
  26. data/lib/google/compute/kernel.rb +20 -0
  27. data/lib/google/compute/listable_resource_collection.rb +33 -0
  28. data/lib/google/compute/machine_type.rb +36 -0
  29. data/lib/google/compute/mixins/utils.rb +58 -0
  30. data/lib/google/compute/network.rb +29 -0
  31. data/lib/google/compute/project.rb +76 -0
  32. data/lib/google/compute/resource.rb +81 -0
  33. data/lib/google/compute/resource_collection.rb +78 -0
  34. data/lib/google/compute/server.rb +87 -0
  35. data/lib/google/compute/server/attached_disk.rb +39 -0
  36. data/lib/google/compute/server/network_interface.rb +38 -0
  37. data/lib/google/compute/server/network_interface/access_config.rb +35 -0
  38. data/lib/google/compute/server/serial_port_output.rb +31 -0
  39. data/lib/google/compute/snapshot.rb +30 -0
  40. data/lib/google/compute/version.rb +19 -0
  41. data/lib/google/compute/zone.rb +32 -0
  42. data/lib/google/compute/zone_operation.rb +60 -0
  43. data/lib/knife-google/version.rb +18 -2
  44. data/spec/chef/knife/google_base_spec.rb +46 -0
  45. data/spec/chef/knife/google_disk_create_spec.rb +36 -0
  46. data/spec/chef/knife/google_disk_delete_spec.rb +65 -0
  47. data/spec/chef/knife/google_disk_list_spec.rb +36 -0
  48. data/spec/chef/knife/google_server_create_spec.rb +84 -0
  49. data/spec/chef/knife/google_server_delete_spec.rb +105 -0
  50. data/spec/chef/knife/google_server_list_spec.rb +39 -0
  51. data/spec/chef/knife/google_setup_spec.rb +25 -0
  52. data/spec/chef/knife/google_zone_list_spec.rb +32 -0
  53. data/spec/data/client.json +14 -0
  54. data/spec/data/compute-v1beta14.json +3386 -0
  55. data/spec/data/disk.json +15 -0
  56. data/spec/data/firewall.json +13 -0
  57. data/spec/data/global_operation.json +36 -0
  58. data/spec/data/image.json +12 -0
  59. data/spec/data/kernel.json +15 -0
  60. data/spec/data/machine_type.json +24 -0
  61. data/spec/data/network.json +10 -0
  62. data/spec/data/project.json +21 -0
  63. data/spec/data/serial_port_output.json +5 -0
  64. data/spec/data/server.json +46 -0
  65. data/spec/data/snapshot.json +12 -0
  66. data/spec/data/zone.json +30 -0
  67. data/spec/data/zone_operation.json +36 -0
  68. data/spec/google/compute/disk_spec.rb +105 -0
  69. data/spec/google/compute/firewall_spec.rb +128 -0
  70. data/spec/google/compute/global_operation_spec.rb +62 -0
  71. data/spec/google/compute/image_spec.rb +75 -0
  72. data/spec/google/compute/kernel_spec.rb +49 -0
  73. data/spec/google/compute/machine_type_spec.rb +53 -0
  74. data/spec/google/compute/network_spec.rb +68 -0
  75. data/spec/google/compute/project_spec.rb +71 -0
  76. data/spec/google/compute/server_spec.rb +125 -0
  77. data/spec/google/compute/snapshot_spec.rb +69 -0
  78. data/spec/google/compute/zone_operation_spec.rb +62 -0
  79. data/spec/google/compute/zone_spec.rb +50 -0
  80. data/spec/spec_helper.rb +44 -0
  81. data/spec/support/mocks.rb +62 -0
  82. data/spec/support/resource_examples.rb +70 -0
  83. data/spec/support/spec_google_base.rb +56 -0
  84. metadata +121 -31
  85. data/README.rdoc +0 -96
@@ -1,99 +1,70 @@
1
- #
2
- # Author:: Chirag Jog (<chiragj@websym.com>)
3
- # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
- # License:: Apache License, Version 2.0
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
5
2
  #
6
3
  # Licensed under the Apache License, Version 2.0 (the "License");
7
4
  # you may not use this file except in compliance with the License.
8
5
  # You may obtain a copy of the License at
9
- #
6
+ #
10
7
  # http://www.apache.org/licenses/LICENSE-2.0
11
- #
8
+ #
12
9
  # Unless required by applicable law or agreed to in writing, software
13
10
  # distributed under the License is distributed on an "AS IS" BASIS,
14
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
12
  # See the License for the specific language governing permissions and
16
13
  # limitations under the License.
17
- #
18
14
 
19
- require 'stringio'
20
- require 'yajl'
21
- require 'mixlib/shellout'
15
+ require 'chef/knife'
16
+ require 'google/compute'
22
17
 
23
- $CLI_PREFIX='gcutil'
24
18
  class Chef
25
19
  class Knife
26
20
  module GoogleBase
27
- @parser = Yajl::Parser.new
28
- @gcompute = nil
29
- @cygwin_path = nil
30
21
 
31
- def is_platform_windows?
32
- return RUBY_PLATFORM.scan('w32').size > 0
33
- end
22
+ # hack for mixlib-cli workaround
23
+ # https://github.com/opscode/knife-ec2/blob/master/lib/chef/knife/ec2_base.rb
24
+ def self.included(includer)
25
+ includer.class_eval do
26
+ deps do
27
+ require 'google/compute'
28
+ require 'chef/json_compat'
29
+ end
34
30
 
35
- def is_cygwin_installed?
36
- ENV['CYGWINPATH'] != nil
31
+ option :compute_credential_file,
32
+ :short => "-f CREDENTIAL_FILE",
33
+ :long => "--compute-credential-file CREDENTIAL_FILE",
34
+ :description => "Google Compute credential file (google setup can create this)"
35
+ end
37
36
  end
38
37
 
39
- def gcompute
40
- return if @gcompute
41
- if is_platform_windows?
42
- if is_cygwin_installed?
43
- #Remove extra quotes
44
- @cygwin_path = ENV['CYGWINPATH'].chomp('\'').reverse.chomp('\'').reverse
45
- #FIXME Generalize the python binary
46
- @gcompute="#{@cygwin_path}\\bin\\python2.6.exe #{@cygwin_path}\\bin\\#{$CLI_PREFIX}"
47
- else
48
- puts "Cannot Find Cygwin Installation !!! Please set CYGWINPATH to point to the Cygwin installation"
49
- exit 1
50
- end
51
- else
52
- Chef::Log.debug("Linux Environment")
53
- @gcompute = $CLI_PREFIX
38
+ def client
39
+ @client ||= begin
40
+ Google::Compute::Client.from_json(config[:compute_credential_file])
54
41
  end
55
42
  end
56
43
 
57
- def parser
58
- if @parser.nil?
59
- @parser = Yajl::Parser.new
44
+ def selflink2name(selflink)
45
+ selflink.to_s == '' ? selflink.to_s : selflink.split('/').last
46
+ end
47
+
48
+ def msg_pair(label, value, color=:cyan)
49
+ if value && !value.to_s.empty?
50
+ ui.info("#{ui.color(label, color)}: #{value}")
60
51
  end
61
52
  end
62
-
63
- def to_json(data)
64
- data_s = StringIO::new(data.strip)
65
- parser.parse(data_s) {|obj| return obj}
53
+
54
+ def disks(instance)
55
+ instance.disks.collect{|d|d.device_name}.compact
66
56
  end
67
57
 
68
- def exec_shell_cmd(cmd)
69
- if is_platform_windows? and is_cygwin_installed?
70
- #Change the HOME PATH From Windows to Cygwin
71
- cygwin_home = "#{@cygwin_path}\\home\\#{ENV['USER']}"
72
-
73
- #Auth token should exist in either ENV['HOME'] or cygwin_home
74
- #XXX Find a way to remove the hard-coded file name
75
- if not File.file?("#{ENV['HOME']}\\.#{$CLI_PREFIX}_auth")
76
- ENV['HOME'] = cygwin_home
77
- end
78
- end
79
- shell_cmd = Mixlib::ShellOut.new(cmd)
80
- shell_cmd.run_command
58
+ def private_ips(instance)
59
+ instance.network_interfaces.collect{|ni|ni.network_ip}.compact
81
60
  end
82
61
 
83
- def validate_project(project_id)
84
- cmd = "#{gcompute} getproject --project_id=#{project_id}"
85
- Chef::Log.debug 'Executing ' + cmd
86
- getprj = exec_shell_cmd(cmd)
87
- if getprj.status.exitstatus > 0
88
- if not getprj.stdout.scan("Enter verification code").empty?
89
- ui.error("Failed to authenticate the account")
90
- ui.error("If not authenticated, please Authenticate gcompute to access the Google Compute Cloud")
91
- ui.error("Authenticate by executing gcutil auth --project_id=<project_id>")
92
- exit 1
93
- end
94
- ui.error("#{getprj.stderr}")
95
- exit 1
96
- end
62
+ def public_ips(instance)
63
+ instance.network_interfaces.collect{|ni|
64
+ ni.access_configs.map{|ac|
65
+ ac.nat_ip
66
+ }
67
+ }.flatten.compact
97
68
  end
98
69
  end
99
70
  end
@@ -0,0 +1,60 @@
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ require 'chef/knife/google_base'
16
+
17
+ class Chef
18
+ class Knife
19
+ class GoogleDiskCreate < Knife
20
+
21
+ include Knife::GoogleBase
22
+
23
+ banner "knife google disk create NAME --google-disk-sizeGb N --google-compute-zone ZONE (options)"
24
+
25
+ deps do
26
+ require 'google/compute'
27
+ end
28
+
29
+ option :zone,
30
+ :short => "-Z ZONE",
31
+ :long => "--google-compute-zone ZONE",
32
+ :description => "The Zone for this disk",
33
+ :required => true
34
+
35
+ option :sizeGb,
36
+ :short => "-s SIZE",
37
+ :long => "--google-disk-sizeGb SIZE",
38
+ :description => "Disk size in GB",
39
+ :required => true
40
+
41
+ def run
42
+ $stdout.sync = true
43
+ unless @name_args.size > 0
44
+ ui.error("Please provide the name of the new disk")
45
+ exit 1
46
+ end
47
+
48
+ begin
49
+ zone = client.zones.get(config[:zone])
50
+ rescue Google::Compute::ResourceNotFound
51
+ ui.error("Zone '#{config[:zone]}' not found")
52
+ exit 1
53
+ end
54
+
55
+ zone_operation = client.disks.create(:name=>@name_args.first,
56
+ :sizeGb=>config[:sizeGb], :zone=>zone.name)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ require 'chef/knife/google_base'
16
+
17
+ class Chef
18
+ class Knife
19
+ class GoogleDiskDelete < Knife
20
+
21
+ include Knife::GoogleBase
22
+
23
+ banner "knife google disk delete NAME --google-compute-zone ZONE"
24
+
25
+ deps do
26
+ require 'google/compute'
27
+ end
28
+
29
+ option :zone,
30
+ :short => "-Z ZONE",
31
+ :long => "--google-compute-zone ZONE",
32
+ :description => "The Zone for this disk",
33
+ :required => true
34
+
35
+ def run
36
+ unless @name_args.size > 0
37
+ ui.error("Please provide the name of the disk to be deleted")
38
+ exit 1
39
+ end
40
+
41
+ begin
42
+ zone = client.zones.get(config[:zone])
43
+ rescue Google::Compute::ResourceNotFound
44
+ ui.error("Zone '#{config[:zone]}' not found")
45
+ exit 1
46
+ end
47
+
48
+ begin
49
+ disk = client.disks.get(:zone=>zone.name, :disk=>@name_args.first)
50
+ ui.confirm("Are you sure you want to delete disk '#{zone.name}:#{disk.name}'")
51
+ zone_operation = client.disks.delete(:zone=>zone.name, :disk=>disk.name)
52
+ ui.warn("Disk '#{zone.name}:#{disk.name}' deleted")
53
+ rescue Google::Compute::ResourceNotFound
54
+ ui.error("Disk '#{zone.name}:#{@name_args.first}' not found")
55
+ exit 1
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,75 @@
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ require 'chef/knife/google_base'
16
+
17
+ class Chef
18
+ class Knife
19
+ class GoogleDiskList < Knife
20
+
21
+ include Knife::GoogleBase
22
+
23
+ banner "knife google disk list --google-compute-zone ZONE (options)"
24
+
25
+ option :zone,
26
+ :short => "-Z ZONE",
27
+ :long => "--google-compute-zone ZONE",
28
+ :description => "The Zone for disk listing",
29
+ :required => true
30
+
31
+ def run
32
+ $stdout.sync = true
33
+
34
+ begin
35
+ zone = client.zones.get(config[:zone])
36
+ rescue Google::Compute::ResourceNotFound
37
+ ui.error("Zone '#{config[:zone]}' not found")
38
+ exit 1
39
+ end
40
+
41
+ disk_list = [
42
+ ui.color("Name", :bold),
43
+ ui.color('Zone', :bold),
44
+ ui.color('Source Snapshot', :bold),
45
+ ui.color('Size (In GB)', :bold),
46
+ ui.color('Status', :bold)].flatten.compact
47
+
48
+ output_column_count = disk_list.length
49
+
50
+ client.disks.list(:zone=>zone.name).each do |disk|
51
+ disk_list << disk.name
52
+ disk_list << selflink2name(disk.zone)
53
+ if disk.source_snapshot.nil?
54
+ disk_list << " "
55
+ else
56
+ selflink2name(disk.source_snapshot)
57
+ end
58
+ disk_list << disk.size_gb
59
+ disk_list << begin
60
+ status = disk.status.downcase
61
+ case status
62
+ when 'stopping', 'stopped', 'terminated'
63
+ ui.color(status, :red)
64
+ when 'requested', 'provisioning', 'staging'
65
+ ui.color(status, :yellow)
66
+ else
67
+ ui.color(status, :green)
68
+ end
69
+ end
70
+ end
71
+ ui.info(ui.list(disk_list, :uneven_columns_across, output_column_count))
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,150 +1,177 @@
1
- # Author:: Chirag Jog (<chiragj@websym.com>)
2
- # Copyright:: Copyright (c) 2012 Opscode, Inc.
3
- # License:: Apache License, Version 2.0
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
4
2
  #
5
3
  # Licensed under the Apache License, Version 2.0 (the "License");
6
4
  # you may not use this file except in compliance with the License.
7
5
  # You may obtain a copy of the License at
8
- #
6
+ #
9
7
  # http://www.apache.org/licenses/LICENSE-2.0
10
- #
8
+ #
11
9
  # Unless required by applicable law or agreed to in writing, software
12
10
  # distributed under the License is distributed on an "AS IS" BASIS,
13
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
12
  # See the License for the specific language governing permissions and
15
13
  # limitations under the License.
16
-
17
- require 'highline'
18
- require 'net/ssh/multi'
19
- require 'net/scp'
20
- require 'tempfile'
21
-
22
- require 'chef/knife'
14
+ #
23
15
  require 'chef/knife/google_base'
24
16
 
25
17
  class Chef
26
18
  class Knife
27
19
  class GoogleServerCreate < Knife
28
20
 
21
+ include Knife::GoogleBase
22
+
29
23
  deps do
30
- require 'readline'
24
+ require 'google/compute'
31
25
  require 'chef/json_compat'
32
26
  require 'chef/knife/bootstrap'
33
27
  Chef::Knife::Bootstrap.load_deps
34
28
  end
35
29
 
36
- include Knife::GoogleBase
30
+ banner "knife google server create NAME -m MACHINE_TYPE -I IMAGE -Z ZONE (options)"
37
31
 
38
- banner "knife google server create NAME [RUN LIST...] (options)"
32
+ attr_accessor :initial_sleep_delay
33
+ attr_reader :instance
39
34
 
40
- option :run_list,
41
- :short => "-r RUN_LIST",
42
- :long => "--run-list RUN_LIST",
43
- :description => "Comma separated list of roles/recipes to apply",
44
- :proc => lambda { |o| o.split(/[\s,]+/) },
45
- :default => []
35
+ option :machine_type,
36
+ :short => "-m MACHINE_TYPE",
37
+ :long => "--google-compute-machine MACHINE_TYPE",
38
+ :description => "The machine type of server (n1-highcpu-2, n1-highcpu-2-d, etc)",
39
+ :required => true
40
+
41
+ option :image,
42
+ :short => "-I IMAGE",
43
+ :long => "--google-compute-image IMAGE",
44
+ :description => "The Image for the server",
45
+ :required => true
46
46
 
47
- option :availability_zone,
47
+ option :zone,
48
48
  :short => "-Z ZONE",
49
- :long => "--availability-zone ZONE",
50
- :description => "The Availability Zone",
51
- :default => "us-east-a",
52
- :proc => Proc.new { |key| Chef::Config[:knife][:availability_zone] = key }
49
+ :long => "--google-compute-zone ZONE",
50
+ :description => "The Zone for this server",
51
+ :required => true
53
52
 
54
- option :distro,
55
- :short => "-d DISTRO",
56
- :long => "--distro DISTRO",
57
- :description => "Bootstrap a distro using a template; default is 'ubuntu10.04-gems'",
58
- :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
59
- :default => "ubuntu10.04-gems"
53
+ option :network,
54
+ :short => "-n NETWORK",
55
+ :long => "--google-compute-network NETWORK",
56
+ :description => "The network for this server; default is 'default'",
57
+ :default => "default"
60
58
 
61
- option :template_file,
62
- :long => "--template-file TEMPLATE",
63
- :description => "Full path to location of template to use",
64
- :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
65
- :default => false
59
+ option :tags,
60
+ :short => "-T TAG1,TAG2,TAG3",
61
+ :long => "--google-compute-tags TAG1,TAG2,TAG3",
62
+ :description => "Tags for this server",
63
+ :proc => Proc.new { |tags| tags.split(',') },
64
+ :default => []
65
+
66
+ option :metadata,
67
+ :short => "-M K=V[,K=V,...]",
68
+ :long => "--google-compute-metadata Key=Value[,Key=Value...]",
69
+ :description => "The metadata for this server",
70
+ :proc => Proc.new { |metadata| metadata.split(',') },
71
+ :default => []
66
72
 
67
73
  option :chef_node_name,
68
74
  :short => "-N NAME",
69
75
  :long => "--node-name NAME",
70
- :description => "The Chef node name for your new node",
71
- :proc => Proc.new { |t| Chef::Config[:knife][:chef_node_name] = t }
76
+ :description => "The Chef node name for your new node"
72
77
 
73
78
  option :ssh_user,
74
79
  :short => "-x USERNAME",
75
80
  :long => "--ssh-user USERNAME",
76
- :description => "The ssh username; default is 'ubuntu'",
77
- :default => "ubuntu"
81
+ :description => "The ssh username; default is 'root'",
82
+ :default => "root"
83
+
84
+ option :ssh_password,
85
+ :short => "-P PASSWORD",
86
+ :long => "--ssh-password PASSWORD",
87
+ :description => "The ssh password"
88
+
89
+ option :ssh_port,
90
+ :short => "-p PORT",
91
+ :long => "--ssh-port PORT",
92
+ :description => "The ssh port; default is '22'",
93
+ :default => "22"
94
+
95
+ option :ssh_gateway,
96
+ :short => "-w GATEWAY",
97
+ :long => "--ssh-gateway GATEWAY",
98
+ :description => "The ssh gateway server"
99
+
100
+ option :identity_file,
101
+ :short => "-i IDENTITY_FILE",
102
+ :long => "--identity-file IDENTITY_FILE",
103
+ :description => "The SSH identity file used for authentication"
104
+
105
+ option :prerelease,
106
+ :long => "--prerelease",
107
+ :description => "Install the pre-release chef gems"
108
+
109
+ option :bootstrap_version,
110
+ :long => "--bootstrap-version VERSION",
111
+ :description => "The version of Chef to install"
78
112
 
79
- option :server_name,
80
- :short => "-N NAME",
81
- :long => "--server-name NAME",
82
- :description => "The server name",
83
- :proc => Proc.new { |server_name| Chef::Config[:knife][:server_name] = server_name }
113
+ option :distro,
114
+ :short => "-d DISTRO",
115
+ :long => "--distro DISTRO",
116
+ :description => "Bootstrap a distro using a template; default is 'chef-full'",
117
+ :default => 'chef-full'
84
118
 
85
- option :flavor,
86
- :short => "-f FLAVOR",
87
- :long => "--flavor FLAVOR",
88
- :description => "The flavor of server (standard-1-cpu,standard-2-cpu-ephemeral-disk, etc)",
89
- :proc => Proc.new { |f| Chef::Config[:knife][:flavor] = f },
90
- :default => "standard-1-cpu"
119
+ option :template_file,
120
+ :long => "--template-file TEMPLATE",
121
+ :description => "Full path to location of template to use",
122
+ :default => false
91
123
 
92
- option :image,
93
- :short => "-I IMAGE",
94
- :long => "--google-image IMAGE",
95
- :description => "Your google virtual app template/image name",
96
- :proc => Proc.new { |template| Chef::Config[:knife][:image] = template },
97
- :default => "gcompute8-standard"
98
-
99
- option :private_key_file,
100
- :short => "-i PRIVATE_KEY_FILE",
101
- :long => "--private-key-file PRIVATE_KEY_FILE",
102
- :description => "The SSH private key file used for authentication",
103
- :proc => Proc.new { |identity| Chef::Config[:knife][:private_key_file] = identity }
104
-
105
- option :public_key_file,
106
- :short => "-k PUBLIC_KEY_FILE",
107
- :long => "--public-key-file PUBLIC_KEY_FILE",
108
- :description => "The SSH public key file used for authentication",
109
- :proc => Proc.new { |identity| Chef::Config[:knife][:public_key_file] = identity }
110
-
111
- option :network,
112
- :short => "-n NETWORKNAME",
113
- :long => "--network NETWORKNAME",
114
- :description => "The Network in which to create the Virtual machine",
115
- :proc => Proc.new { |network| Chef::Config[:knife][:network] = network},
116
- :default => "default"
124
+ option :run_list,
125
+ :short => "-r RUN_LIST",
126
+ :long => "--run-list RUN_LIST",
127
+ :description => "Comma separated list of roles/recipes to apply",
128
+ :proc => lambda { |o| o.split(/[\s,]+/) }
129
+
130
+ option :json_attributes,
131
+ :short => "-j JSON",
132
+ :long => "--json-attributes JSON",
133
+ :description => "A JSON string to be added to the first run of chef-client",
134
+ :proc => lambda { |o| JSON.parse(o) }
135
+
136
+ option :host_key_verify,
137
+ :long => "--[no-]host-key-verify",
138
+ :description => "Verify host key, enabled by default.",
139
+ :boolean => true,
140
+ :default => true
141
+
142
+ option :compute_user_data,
143
+ :long => "--user-data USER_DATA_FILE",
144
+ :short => "-u USER_DATA_FILE",
145
+ :description => "The Google Compute User Data file to provision the server with"
146
+
147
+ option :hint,
148
+ :long => "--hint HINT_NAME[=HINT_FILE]",
149
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
150
+ :proc => Proc.new { |h|
151
+ Chef::Config[:knife][:hints] ||= {}
152
+ name, path = h.split("=")
153
+ Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new
154
+ }
155
+
156
+ option :instance_connect_ip,
157
+ :long => "--google-compute-server-connect-ip PUBLIC",
158
+ :short => "-a PUBLIC",
159
+ :description => "Whether to use PUBLIC or PRIVATE address to connect; default is 'PUBLIC'",
160
+ :default => 'PUBLIC'
161
+
162
+ option :disks,
163
+ :long=> "--google-compute-disks DISK1,DISK2",
164
+ :proc => Proc.new { |metadata| metadata.split(',') },
165
+ :description => "Disks to be attached",
166
+ :default => []
117
167
 
118
- option :external_ip_address,
119
- :short => "-e IPADDRESS",
120
- :long => "--external-ip-address IPADDRESS",
121
- :description => "A Static IP provided by Google",
122
- :proc => Proc.new { |ipaddr| Chef::Config[:knife][:external_ip_address] = ipaddr},
123
- :default => "ephemeral"
124
-
125
- option :internal_ip_address,
126
- :short => "-P IPADDRESS",
127
- :long => "--internal-ip-address IPADDRESS",
128
- :description => "A Static IP provided by Google",
129
- :proc => Proc.new { |ipaddr| Chef::Config[:knife][:internal_ip_address] = ipaddr}
130
-
131
- option :project,
132
- :short => "-p PROJECT",
133
- :long => "--project_id PROJECT",
134
- :description => "Google Compute Project",
135
- :proc => Proc.new { |project| Chef::Config[:knife][:google_project] = project}
136
-
137
- def h
138
- @highline ||= HighLine.new
139
- end
140
-
141
- def locate_config_value(key)
142
- key = key.to_sym
143
- config[key] || Chef::Config[:knife][key]
144
- end
168
+ option :public_ip,
169
+ :long=> "--google-compute-public-ip IP_ADDRESS",
170
+ :description => "EPHEMERAL or static IP address or NONE; default is 'EPHEMERAL'",
171
+ :default => "EPHEMERAL"
145
172
 
146
- def tcp_test_ssh(hostname, port)
147
- tcp_socket = TCPSocket.new(hostname, port)
173
+ def tcp_test_ssh(hostname, ssh_port)
174
+ tcp_socket = TCPSocket.new(hostname, ssh_port)
148
175
  readable = IO.select([tcp_socket], nil, nil, 5)
149
176
  if readable
150
177
  Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
@@ -153,108 +180,170 @@ class Chef
153
180
  else
154
181
  false
155
182
  end
156
- rescue Errno::ETIMEDOUT
157
- false
158
- rescue Errno::EPERM
159
- false
160
- rescue Errno::ECONNREFUSED
183
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
161
184
  sleep 2
162
185
  false
163
- rescue Errno::EHOSTUNREACH
164
- sleep 2
186
+ rescue Errno::EPERM, Errno::ETIMEDOUT
165
187
  false
166
188
  ensure
167
189
  tcp_socket && tcp_socket.close
168
190
  end
169
191
 
170
- def run
171
- unless Chef::Config[:knife][:server_name]
172
- ui.error("Server Name is a compulsory parameter")
173
- exit 1
174
- end
192
+ def wait_for_sshd(hostname)
193
+ config[:ssh_gateway] ? wait_for_tunnelled_sshd(hostname) : wait_for_direct_sshd(hostname, config[:ssh_port])
194
+ end
175
195
 
176
- unless Chef::Config[:knife][:public_key_file]
177
- ui.error("SSH public key file is a compulsory parameter")
178
- exit 1
196
+ def wait_for_tunnelled_sshd(hostname)
197
+ print(".")
198
+ print(".") until tunnel_test_ssh(ssh_connect_host) {
199
+ sleep @initial_sleep_delay ||= 40
200
+ puts("done")
201
+ }
202
+ end
203
+
204
+ def tunnel_test_ssh(hostname, &block)
205
+ gw_host, gw_user = config[:ssh_gateway].split('@').reverse
206
+ gw_host, gw_port = gw_host.split(':')
207
+ gateway = Net::SSH::Gateway.new(gw_host, gw_user, :port => gw_port || 22)
208
+ status = false
209
+ gateway.open(hostname, config[:ssh_port]) do |local_tunnel_port|
210
+ status = tcp_test_ssh('localhost', local_tunnel_port, &block)
179
211
  end
212
+ status
213
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
214
+ sleep 2
215
+ false
216
+ rescue Errno::EPERM, Errno::ETIMEDOUT
217
+ false
218
+ end
180
219
 
181
- unless Chef::Config[:knife][:google_project]
182
- ui.error("Project ID is a compulsory parameter")
183
- exit 1
220
+ def wait_for_direct_sshd(hostname, ssh_port)
221
+ print(".") until tcp_test_ssh(ssh_connect_host, ssh_port) {
222
+ sleep @initial_sleep_delay ||= 40
223
+ puts("done")
224
+ }
225
+ end
226
+
227
+ def ssh_connect_host
228
+ @ssh_connect_host ||= if config[:instance_connect_ip] == 'PUBLIC'
229
+ public_ips(@instance).first
230
+ else
231
+ private_ips(@instance).first
184
232
  end
233
+ end
185
234
 
235
+ def bootstrap_for_node(instance,ssh_host)
236
+ bootstrap = Chef::Knife::Bootstrap.new
237
+ bootstrap.name_args = [ssh_host]
238
+ bootstrap.config[:run_list] = config[:run_list]
239
+ bootstrap.config[:ssh_user] = config[:ssh_user]
240
+ bootstrap.config[:ssh_port] = config[:ssh_port]
241
+ bootstrap.config[:ssh_gateway] = config[:ssh_gateway]
242
+ bootstrap.config[:identity_file] = config[:identity_file]
243
+ bootstrap.config[:chef_node_name] = config[:chef_node_name] || instance.name
244
+ bootstrap.config[:prerelease] = config[:prerelease]
245
+ bootstrap.config[:bootstrap_version] = config[:bootstrap_version]
246
+ bootstrap.config[:first_boot_attributes] = config[:json_attributes]
247
+ bootstrap.config[:distro] = config[:distro]
248
+ bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
249
+ bootstrap.config[:template_file] = config[:template_file]
250
+ bootstrap.config[:environment] = config[:environment]
251
+ # may be needed for vpc_mode
252
+ bootstrap.config[:host_key_verify] = config[:host_key_verify]
253
+ # Modify global configuration state to ensure hint gets set by
254
+ # knife-bootstrap
255
+ Chef::Config[:knife][:hints] ||= {}
256
+ Chef::Config[:knife][:hints]["google"] ||= {}
257
+ bootstrap
258
+ end
259
+
260
+ def run
186
261
  $stdout.sync = true
262
+ unless @name_args.size > 0
263
+ ui.error("Please provide the name of the new server")
264
+ exit 1
265
+ end
187
266
 
188
- project_id = Chef::Config[:knife][:google_project]
189
- validate_project(project_id)
190
-
191
- server_name = Chef::Config[:knife][:server_name]
192
- image = Chef::Config[:knife][:image]
193
- key_file = locate_config_value(:public_key_file)
194
- network = locate_config_value(:network)
195
- flavor = locate_config_value(:flavor)
196
- zone = locate_config_value(:availability_zone)
197
- user = locate_config_value(:ssh_user)
198
- external_ip_address = locate_config_value(:external_ip_address)
199
- internal_ip_address = locate_config_value(:internal_ip_address) || nil
200
- puts "\n#{ui.color("Waiting for the server to be Instantiated", :magenta)}"
201
- cmd_add_instance = "#{@gcompute} addinstance #{server_name} --machine_type #{flavor} " +
202
- "--zone #{zone} --project_id #{project_id} --tags #{server_name} " +
203
- "--authorized_ssh_keys #{user}:#{key_file} --network #{network} " +
204
- "--external_ip_address #{external_ip_address} --print_json"
205
- cmd_add_instance << " --internal_ip_address #{internal_ip_address}" if internal_ip_address
206
-
207
- Chef::Log.debug 'Executing ' + cmd_add_instance
208
- create_server = exec_shell_cmd(cmd_add_instance)
209
-
210
- if create_server.stderr.downcase.scan("error").size > 0
211
- ui.error("\nFailed to create server: #{create_server.stderr}")
267
+ begin
268
+ zone = client.zones.get(config[:zone]).self_link
269
+ rescue Google::Compute::ResourceNotFound
270
+ ui.error("Zone '#{config[:zone]}' not found")
212
271
  exit 1
213
272
  end
214
- if create_server.stdout.downcase.scan("error").size > 0
215
- output = to_json(create_server.stdout)
216
- ui.error("\nFailed to create server: #{output["error"]}")
273
+ begin
274
+ machine_type = client.machine_types.get(config[:machine_type]).self_link
275
+ rescue Google::Compute::ResourceNotFound
276
+ ui.error("MachineType '#{config[:machine_type]}' not found")
217
277
  exit 1
218
278
  end
219
-
220
- #Fetch server information
221
- cmd_get_instance = "#{@gcompute} getinstance #{server_name} --project_id #{project_id} --print_json "
222
- Chef::Log.debug 'Executing ' + cmd_get_instance
223
- get_instance = exec_shell_cmd(cmd_get_instance)
224
-
225
- if not get_instance.stderr.downcase.scan("error").empty?
226
- ui.error("Failed to fetch server details.")
279
+ begin
280
+ image = client.images.get(:project=>'google', :name=>config[:image]).self_link
281
+ rescue Google::Compute::ResourceNotFound
282
+ ui.error("Image '#{config[:image]}' not found")
283
+ exit 1
284
+ end
285
+ begin
286
+ network = client.networks.get(config[:network]).self_link
287
+ rescue Google::Compute::ResourceNotFound
288
+ ui.error("Network '#{config[:network]}' not found")
227
289
  exit 1
228
290
  end
229
291
 
230
- server = to_json(get_instance.stdout)
231
- private_ip = []
232
- public_ip = []
233
- server["networkInterfaces"].each do |interface|
234
- private_ip << interface["networkIP"]
235
- interface["accessConfigs"].select { |cfg| public_ip << cfg["natIP"] }
292
+ disks = config[:disks].collect{|disk| client.disks.get(:disk=>disk, :zone=>selflink2name(zone)).self_link}
293
+ metadata = config[:metadata].collect{|pair| Hash[*pair.split('=')] }
294
+ network_interface = {'network'=>network}
295
+
296
+ if config[:public_ip] == 'EPHEMERAL'
297
+ network_interface.merge!('accessConfigs' =>[{"name"=>"External NAT",
298
+ "type"=> "ONE_TO_ONE_NAT"}])
299
+ elsif config[:public_ip] =~ /\d+\.\d+\.\d+\.\d+/
300
+ network_interface.merge!('accessConfigs' =>[{"name"=>"External NAT",
301
+ "type"=>"ONE_TO_ONE_NAT", "natIP"=>config[:public_ip] }])
302
+ elsif config[:public_ip] == 'NONE'
303
+ # do nothing
304
+ else
305
+ ui.error("Invalid public ip value : #{config[:public_ip]}")
306
+ exit 1
307
+ end
308
+ zone_operation = client.instances.create(:name=>@name_args.first, :zone=>selflink2name(zone),
309
+ :image=> image,
310
+ :machineType =>machine_type,
311
+ :disks=>disks,
312
+ :metadata=>{'items'=> metadata },
313
+ :networkInterfaces => [network_interface],
314
+ :tags=> config[:tags]
315
+ )
316
+
317
+ ui.info("Waiting for the create server operation to complete")
318
+ until zone_operation.progress.to_i == 100
319
+ ui.info(".")
320
+ sleep 1
321
+ zone_operation = client.zoneOperations.get(:name=>zone_operation, :operation=>zone_operation.name, :zone=>selflink2name(zone))
322
+ end
323
+ ui.info("Waiting for the servers to be in running state")
324
+
325
+ @instance = client.instances.get(:name=>@name_args.first, :zone=>selflink2name(zone))
326
+ msg_pair("Instance Name", @instance.name)
327
+ msg_pair("MachineType", selflink2name(@instance.machine_type))
328
+ msg_pair("Image", selflink2name(@instance.image))
329
+ msg_pair("Zone", selflink2name(@instance.zone))
330
+ msg_pair("Tags", @instance.tags.has_key?("items") ? @instance.tags["items"].join(",") : "None")
331
+ until @instance.status == "RUNNING"
332
+ sleep 3
333
+ msg_pair("Status", @instance.status.downcase)
334
+ @instance = client.instances.get(:name=>@name_args.first, :zone=>selflink2name(zone))
236
335
  end
237
336
 
238
- puts "#{ui.color("Public IP Address", :cyan)}: #{public_ip[0]}"
239
- puts "#{ui.color("Private IP Address", :cyan)}: #{private_ip[0]}"
240
- puts "\n#{ui.color("Waiting for sshd.", :magenta)}"
241
- puts(".") until tcp_test_ssh(public_ip[0], "22") { sleep @initial_sleep_delay ||= 10; puts("done") }
242
- puts "\nBootstrapping #{h.color(server_name, :bold)}..."
243
- bootstrap_for_node(server_name, public_ip[0]).run
244
- end
337
+ msg_pair("Public IP Address", public_ips(@instance)) unless public_ips(@instance).empty?
338
+ msg_pair("Private IP Address", private_ips(@instance))
339
+ ui.info("\n#{ui.color("Waiting for server", :magenta)}")
245
340
 
246
- def bootstrap_for_node(server_name, public_ip)
247
- bootstrap = Chef::Knife::Bootstrap.new
248
- bootstrap.name_args = [public_ip]
249
- bootstrap.config[:run_list] = locate_config_value(:run_list)
250
- bootstrap.config[:ssh_user] = locate_config_value(:ssh_user) || "root"
251
- bootstrap.config[:identity_file] = locate_config_value(:private_key_file)
252
- bootstrap.config[:chef_node_name] = locate_config_value(:chef_node_name) || server_name
253
- bootstrap.config[:distro] = locate_config_value(:distro)
254
- bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
255
- bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
256
- bootstrap.config[:template_file] = locate_config_value(:template_file)
257
- bootstrap
341
+ ui.info("\n")
342
+ ui.info(ui.color("Waiting for sshd", :magenta))
343
+ wait_for_sshd(ssh_connect_host)
344
+ bootstrap_for_node(@instance,ssh_connect_host).run
345
+ ui.info("\n")
346
+ ui.info("Complete!!")
258
347
  end
259
348
  end
260
349
  end