lib/chef/knife/google_server_create.rb in knife-google-0.0.1 vs lib/chef/knife/google_server_create.rb in knife-google-1.0.0
- old
+ new
@@ -1,261 +1,350 @@
-# Author:: Chirag Jog (<chiragj@websym.com>)
-# Copyright:: Copyright (c) 2012 Opscode, Inc.
-# License:: Apache License, Version 2.0
+# Copyright 2013 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
-#
+#
# http://www.apache.org/licenses/LICENSE-2.0
-#
+#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-
-require 'highline'
-require 'net/ssh/multi'
-require 'net/scp'
-require 'tempfile'
-
-require 'chef/knife'
+#
require 'chef/knife/google_base'
class Chef
class Knife
class GoogleServerCreate < Knife
+ include Knife::GoogleBase
+
deps do
- require 'readline'
+ require 'google/compute'
require 'chef/json_compat'
require 'chef/knife/bootstrap'
Chef::Knife::Bootstrap.load_deps
end
- include Knife::GoogleBase
+ banner "knife google server create NAME -m MACHINE_TYPE -I IMAGE -Z ZONE (options)"
- banner "knife google server create NAME [RUN LIST...] (options)"
+ attr_accessor :initial_sleep_delay
+ attr_reader :instance
- option :run_list,
- :short => "-r RUN_LIST",
- :long => "--run-list RUN_LIST",
- :description => "Comma separated list of roles/recipes to apply",
- :proc => lambda { |o| o.split(/[\s,]+/) },
- :default => []
+ option :machine_type,
+ :short => "-m MACHINE_TYPE",
+ :long => "--google-compute-machine MACHINE_TYPE",
+ :description => "The machine type of server (n1-highcpu-2, n1-highcpu-2-d, etc)",
+ :required => true
- option :availability_zone,
+ option :image,
+ :short => "-I IMAGE",
+ :long => "--google-compute-image IMAGE",
+ :description => "The Image for the server",
+ :required => true
+
+ option :zone,
:short => "-Z ZONE",
- :long => "--availability-zone ZONE",
- :description => "The Availability Zone",
- :default => "us-east-a",
- :proc => Proc.new { |key| Chef::Config[:knife][:availability_zone] = key }
+ :long => "--google-compute-zone ZONE",
+ :description => "The Zone for this server",
+ :required => true
- option :distro,
- :short => "-d DISTRO",
- :long => "--distro DISTRO",
- :description => "Bootstrap a distro using a template; default is 'ubuntu10.04-gems'",
- :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d },
- :default => "ubuntu10.04-gems"
+ option :network,
+ :short => "-n NETWORK",
+ :long => "--google-compute-network NETWORK",
+ :description => "The network for this server; default is 'default'",
+ :default => "default"
- option :template_file,
- :long => "--template-file TEMPLATE",
- :description => "Full path to location of template to use",
- :proc => Proc.new { |t| Chef::Config[:knife][:template_file] = t },
- :default => false
+ option :tags,
+ :short => "-T TAG1,TAG2,TAG3",
+ :long => "--google-compute-tags TAG1,TAG2,TAG3",
+ :description => "Tags for this server",
+ :proc => Proc.new { |tags| tags.split(',') },
+ :default => []
+ option :metadata,
+ :short => "-M K=V[,K=V,...]",
+ :long => "--google-compute-metadata Key=Value[,Key=Value...]",
+ :description => "The metadata for this server",
+ :proc => Proc.new { |metadata| metadata.split(',') },
+ :default => []
+
option :chef_node_name,
:short => "-N NAME",
:long => "--node-name NAME",
- :description => "The Chef node name for your new node",
- :proc => Proc.new { |t| Chef::Config[:knife][:chef_node_name] = t }
+ :description => "The Chef node name for your new node"
option :ssh_user,
:short => "-x USERNAME",
:long => "--ssh-user USERNAME",
- :description => "The ssh username; default is 'ubuntu'",
- :default => "ubuntu"
+ :description => "The ssh username; default is 'root'",
+ :default => "root"
- option :server_name,
- :short => "-N NAME",
- :long => "--server-name NAME",
- :description => "The server name",
- :proc => Proc.new { |server_name| Chef::Config[:knife][:server_name] = server_name }
+ option :ssh_password,
+ :short => "-P PASSWORD",
+ :long => "--ssh-password PASSWORD",
+ :description => "The ssh password"
- option :flavor,
- :short => "-f FLAVOR",
- :long => "--flavor FLAVOR",
- :description => "The flavor of server (standard-1-cpu,standard-2-cpu-ephemeral-disk, etc)",
- :proc => Proc.new { |f| Chef::Config[:knife][:flavor] = f },
- :default => "standard-1-cpu"
+ option :ssh_port,
+ :short => "-p PORT",
+ :long => "--ssh-port PORT",
+ :description => "The ssh port; default is '22'",
+ :default => "22"
- option :image,
- :short => "-I IMAGE",
- :long => "--google-image IMAGE",
- :description => "Your google virtual app template/image name",
- :proc => Proc.new { |template| Chef::Config[:knife][:image] = template },
- :default => "gcompute8-standard"
-
- option :private_key_file,
- :short => "-i PRIVATE_KEY_FILE",
- :long => "--private-key-file PRIVATE_KEY_FILE",
- :description => "The SSH private key file used for authentication",
- :proc => Proc.new { |identity| Chef::Config[:knife][:private_key_file] = identity }
-
- option :public_key_file,
- :short => "-k PUBLIC_KEY_FILE",
- :long => "--public-key-file PUBLIC_KEY_FILE",
- :description => "The SSH public key file used for authentication",
- :proc => Proc.new { |identity| Chef::Config[:knife][:public_key_file] = identity }
-
- option :network,
- :short => "-n NETWORKNAME",
- :long => "--network NETWORKNAME",
- :description => "The Network in which to create the Virtual machine",
- :proc => Proc.new { |network| Chef::Config[:knife][:network] = network},
- :default => "default"
+ option :ssh_gateway,
+ :short => "-w GATEWAY",
+ :long => "--ssh-gateway GATEWAY",
+ :description => "The ssh gateway server"
- option :external_ip_address,
- :short => "-e IPADDRESS",
- :long => "--external-ip-address IPADDRESS",
- :description => "A Static IP provided by Google",
- :proc => Proc.new { |ipaddr| Chef::Config[:knife][:external_ip_address] = ipaddr},
- :default => "ephemeral"
+ option :identity_file,
+ :short => "-i IDENTITY_FILE",
+ :long => "--identity-file IDENTITY_FILE",
+ :description => "The SSH identity file used for authentication"
- option :internal_ip_address,
- :short => "-P IPADDRESS",
- :long => "--internal-ip-address IPADDRESS",
- :description => "A Static IP provided by Google",
- :proc => Proc.new { |ipaddr| Chef::Config[:knife][:internal_ip_address] = ipaddr}
+ option :prerelease,
+ :long => "--prerelease",
+ :description => "Install the pre-release chef gems"
- option :project,
- :short => "-p PROJECT",
- :long => "--project_id PROJECT",
- :description => "Google Compute Project",
- :proc => Proc.new { |project| Chef::Config[:knife][:google_project] = project}
+ option :bootstrap_version,
+ :long => "--bootstrap-version VERSION",
+ :description => "The version of Chef to install"
- def h
- @highline ||= HighLine.new
- end
-
- def locate_config_value(key)
- key = key.to_sym
- config[key] || Chef::Config[:knife][key]
- end
+ option :distro,
+ :short => "-d DISTRO",
+ :long => "--distro DISTRO",
+ :description => "Bootstrap a distro using a template; default is 'chef-full'",
+ :default => 'chef-full'
- def tcp_test_ssh(hostname, port)
- tcp_socket = TCPSocket.new(hostname, port)
+ option :template_file,
+ :long => "--template-file TEMPLATE",
+ :description => "Full path to location of template to use",
+ :default => false
+
+ option :run_list,
+ :short => "-r RUN_LIST",
+ :long => "--run-list RUN_LIST",
+ :description => "Comma separated list of roles/recipes to apply",
+ :proc => lambda { |o| o.split(/[\s,]+/) }
+
+ option :json_attributes,
+ :short => "-j JSON",
+ :long => "--json-attributes JSON",
+ :description => "A JSON string to be added to the first run of chef-client",
+ :proc => lambda { |o| JSON.parse(o) }
+
+ option :host_key_verify,
+ :long => "--[no-]host-key-verify",
+ :description => "Verify host key, enabled by default.",
+ :boolean => true,
+ :default => true
+
+ option :compute_user_data,
+ :long => "--user-data USER_DATA_FILE",
+ :short => "-u USER_DATA_FILE",
+ :description => "The Google Compute User Data file to provision the server with"
+
+ option :hint,
+ :long => "--hint HINT_NAME[=HINT_FILE]",
+ :description => "Specify Ohai Hint to be set on the bootstrap target. Use multiple --hint options to specify multiple hints.",
+ :proc => Proc.new { |h|
+ Chef::Config[:knife][:hints] ||= {}
+ name, path = h.split("=")
+ Chef::Config[:knife][:hints][name] = path ? JSON.parse(::File.read(path)) : Hash.new
+ }
+
+ option :instance_connect_ip,
+ :long => "--google-compute-server-connect-ip PUBLIC",
+ :short => "-a PUBLIC",
+ :description => "Whether to use PUBLIC or PRIVATE address to connect; default is 'PUBLIC'",
+ :default => 'PUBLIC'
+
+ option :disks,
+ :long=> "--google-compute-disks DISK1,DISK2",
+ :proc => Proc.new { |metadata| metadata.split(',') },
+ :description => "Disks to be attached",
+ :default => []
+
+ option :public_ip,
+ :long=> "--google-compute-public-ip IP_ADDRESS",
+ :description => "EPHEMERAL or static IP address or NONE; default is 'EPHEMERAL'",
+ :default => "EPHEMERAL"
+
+ def tcp_test_ssh(hostname, ssh_port)
+ tcp_socket = TCPSocket.new(hostname, ssh_port)
readable = IO.select([tcp_socket], nil, nil, 5)
if readable
Chef::Log.debug("sshd accepting connections on #{hostname}, banner is #{tcp_socket.gets}")
yield
true
else
false
end
- rescue Errno::ETIMEDOUT
- false
- rescue Errno::EPERM
- false
- rescue Errno::ECONNREFUSED
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
sleep 2
false
- rescue Errno::EHOSTUNREACH
- sleep 2
+ rescue Errno::EPERM, Errno::ETIMEDOUT
false
ensure
tcp_socket && tcp_socket.close
end
- def run
- unless Chef::Config[:knife][:server_name]
- ui.error("Server Name is a compulsory parameter")
- exit 1
- end
+ def wait_for_sshd(hostname)
+ config[:ssh_gateway] ? wait_for_tunnelled_sshd(hostname) : wait_for_direct_sshd(hostname, config[:ssh_port])
+ end
- unless Chef::Config[:knife][:public_key_file]
- ui.error("SSH public key file is a compulsory parameter")
- exit 1
- end
+ def wait_for_tunnelled_sshd(hostname)
+ print(".")
+ print(".") until tunnel_test_ssh(ssh_connect_host) {
+ sleep @initial_sleep_delay ||= 40
+ puts("done")
+ }
+ end
- unless Chef::Config[:knife][:google_project]
- ui.error("Project ID is a compulsory parameter")
- exit 1
+ def tunnel_test_ssh(hostname, &block)
+ gw_host, gw_user = config[:ssh_gateway].split('@').reverse
+ gw_host, gw_port = gw_host.split(':')
+ gateway = Net::SSH::Gateway.new(gw_host, gw_user, :port => gw_port || 22)
+ status = false
+ gateway.open(hostname, config[:ssh_port]) do |local_tunnel_port|
+ status = tcp_test_ssh('localhost', local_tunnel_port, &block)
end
+ status
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, IOError
+ sleep 2
+ false
+ rescue Errno::EPERM, Errno::ETIMEDOUT
+ false
+ end
- $stdout.sync = true
+ def wait_for_direct_sshd(hostname, ssh_port)
+ print(".") until tcp_test_ssh(ssh_connect_host, ssh_port) {
+ sleep @initial_sleep_delay ||= 40
+ puts("done")
+ }
+ end
- project_id = Chef::Config[:knife][:google_project]
- validate_project(project_id)
+ def ssh_connect_host
+ @ssh_connect_host ||= if config[:instance_connect_ip] == 'PUBLIC'
+ public_ips(@instance).first
+ else
+ private_ips(@instance).first
+ end
+ end
- server_name = Chef::Config[:knife][:server_name]
- image = Chef::Config[:knife][:image]
- key_file = locate_config_value(:public_key_file)
- network = locate_config_value(:network)
- flavor = locate_config_value(:flavor)
- zone = locate_config_value(:availability_zone)
- user = locate_config_value(:ssh_user)
- external_ip_address = locate_config_value(:external_ip_address)
- internal_ip_address = locate_config_value(:internal_ip_address) || nil
- puts "\n#{ui.color("Waiting for the server to be Instantiated", :magenta)}"
- cmd_add_instance = "#{@gcompute} addinstance #{server_name} --machine_type #{flavor} " +
- "--zone #{zone} --project_id #{project_id} --tags #{server_name} " +
- "--authorized_ssh_keys #{user}:#{key_file} --network #{network} " +
- "--external_ip_address #{external_ip_address} --print_json"
- cmd_add_instance << " --internal_ip_address #{internal_ip_address}" if internal_ip_address
+ def bootstrap_for_node(instance,ssh_host)
+ bootstrap = Chef::Knife::Bootstrap.new
+ bootstrap.name_args = [ssh_host]
+ bootstrap.config[:run_list] = config[:run_list]
+ bootstrap.config[:ssh_user] = config[:ssh_user]
+ bootstrap.config[:ssh_port] = config[:ssh_port]
+ bootstrap.config[:ssh_gateway] = config[:ssh_gateway]
+ bootstrap.config[:identity_file] = config[:identity_file]
+ bootstrap.config[:chef_node_name] = config[:chef_node_name] || instance.name
+ bootstrap.config[:prerelease] = config[:prerelease]
+ bootstrap.config[:bootstrap_version] = config[:bootstrap_version]
+ bootstrap.config[:first_boot_attributes] = config[:json_attributes]
+ bootstrap.config[:distro] = config[:distro]
+ bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
+ bootstrap.config[:template_file] = config[:template_file]
+ bootstrap.config[:environment] = config[:environment]
+ # may be needed for vpc_mode
+ bootstrap.config[:host_key_verify] = config[:host_key_verify]
+ # Modify global configuration state to ensure hint gets set by
+ # knife-bootstrap
+ Chef::Config[:knife][:hints] ||= {}
+ Chef::Config[:knife][:hints]["google"] ||= {}
+ bootstrap
+ end
- Chef::Log.debug 'Executing ' + cmd_add_instance
- create_server = exec_shell_cmd(cmd_add_instance)
+ def run
+ $stdout.sync = true
+ unless @name_args.size > 0
+ ui.error("Please provide the name of the new server")
+ exit 1
+ end
- if create_server.stderr.downcase.scan("error").size > 0
- ui.error("\nFailed to create server: #{create_server.stderr}")
+ begin
+ zone = client.zones.get(config[:zone]).self_link
+ rescue Google::Compute::ResourceNotFound
+ ui.error("Zone '#{config[:zone]}' not found")
exit 1
end
- if create_server.stdout.downcase.scan("error").size > 0
- output = to_json(create_server.stdout)
- ui.error("\nFailed to create server: #{output["error"]}")
+ begin
+ machine_type = client.machine_types.get(config[:machine_type]).self_link
+ rescue Google::Compute::ResourceNotFound
+ ui.error("MachineType '#{config[:machine_type]}' not found")
exit 1
end
-
- #Fetch server information
- cmd_get_instance = "#{@gcompute} getinstance #{server_name} --project_id #{project_id} --print_json "
- Chef::Log.debug 'Executing ' + cmd_get_instance
- get_instance = exec_shell_cmd(cmd_get_instance)
+ begin
+ image = client.images.get(:project=>'google', :name=>config[:image]).self_link
+ rescue Google::Compute::ResourceNotFound
+ ui.error("Image '#{config[:image]}' not found")
+ exit 1
+ end
+ begin
+ network = client.networks.get(config[:network]).self_link
+ rescue Google::Compute::ResourceNotFound
+ ui.error("Network '#{config[:network]}' not found")
+ exit 1
+ end
- if not get_instance.stderr.downcase.scan("error").empty?
- ui.error("Failed to fetch server details.")
+ disks = config[:disks].collect{|disk| client.disks.get(:disk=>disk, :zone=>selflink2name(zone)).self_link}
+ metadata = config[:metadata].collect{|pair| Hash[*pair.split('=')] }
+ network_interface = {'network'=>network}
+
+ if config[:public_ip] == 'EPHEMERAL'
+ network_interface.merge!('accessConfigs' =>[{"name"=>"External NAT",
+ "type"=> "ONE_TO_ONE_NAT"}])
+ elsif config[:public_ip] =~ /\d+\.\d+\.\d+\.\d+/
+ network_interface.merge!('accessConfigs' =>[{"name"=>"External NAT",
+ "type"=>"ONE_TO_ONE_NAT", "natIP"=>config[:public_ip] }])
+ elsif config[:public_ip] == 'NONE'
+ # do nothing
+ else
+ ui.error("Invalid public ip value : #{config[:public_ip]}")
exit 1
end
+ zone_operation = client.instances.create(:name=>@name_args.first, :zone=>selflink2name(zone),
+ :image=> image,
+ :machineType =>machine_type,
+ :disks=>disks,
+ :metadata=>{'items'=> metadata },
+ :networkInterfaces => [network_interface],
+ :tags=> config[:tags]
+ )
- server = to_json(get_instance.stdout)
- private_ip = []
- public_ip = []
- server["networkInterfaces"].each do |interface|
- private_ip << interface["networkIP"]
- interface["accessConfigs"].select { |cfg| public_ip << cfg["natIP"] }
+ ui.info("Waiting for the create server operation to complete")
+ until zone_operation.progress.to_i == 100
+ ui.info(".")
+ sleep 1
+ zone_operation = client.zoneOperations.get(:name=>zone_operation, :operation=>zone_operation.name, :zone=>selflink2name(zone))
end
+ ui.info("Waiting for the servers to be in running state")
- puts "#{ui.color("Public IP Address", :cyan)}: #{public_ip[0]}"
- puts "#{ui.color("Private IP Address", :cyan)}: #{private_ip[0]}"
- puts "\n#{ui.color("Waiting for sshd.", :magenta)}"
- puts(".") until tcp_test_ssh(public_ip[0], "22") { sleep @initial_sleep_delay ||= 10; puts("done") }
- puts "\nBootstrapping #{h.color(server_name, :bold)}..."
- bootstrap_for_node(server_name, public_ip[0]).run
- end
+ @instance = client.instances.get(:name=>@name_args.first, :zone=>selflink2name(zone))
+ msg_pair("Instance Name", @instance.name)
+ msg_pair("MachineType", selflink2name(@instance.machine_type))
+ msg_pair("Image", selflink2name(@instance.image))
+ msg_pair("Zone", selflink2name(@instance.zone))
+ msg_pair("Tags", @instance.tags.has_key?("items") ? @instance.tags["items"].join(",") : "None")
+ until @instance.status == "RUNNING"
+ sleep 3
+ msg_pair("Status", @instance.status.downcase)
+ @instance = client.instances.get(:name=>@name_args.first, :zone=>selflink2name(zone))
+ end
- def bootstrap_for_node(server_name, public_ip)
- bootstrap = Chef::Knife::Bootstrap.new
- bootstrap.name_args = [public_ip]
- bootstrap.config[:run_list] = locate_config_value(:run_list)
- bootstrap.config[:ssh_user] = locate_config_value(:ssh_user) || "root"
- bootstrap.config[:identity_file] = locate_config_value(:private_key_file)
- bootstrap.config[:chef_node_name] = locate_config_value(:chef_node_name) || server_name
- bootstrap.config[:distro] = locate_config_value(:distro)
- bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version)
- bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root'
- bootstrap.config[:template_file] = locate_config_value(:template_file)
- bootstrap
+ msg_pair("Public IP Address", public_ips(@instance)) unless public_ips(@instance).empty?
+ msg_pair("Private IP Address", private_ips(@instance))
+ ui.info("\n#{ui.color("Waiting for server", :magenta)}")
+
+ ui.info("\n")
+ ui.info(ui.color("Waiting for sshd", :magenta))
+ wait_for_sshd(ssh_connect_host)
+ bootstrap_for_node(@instance,ssh_connect_host).run
+ ui.info("\n")
+ ui.info("Complete!!")
end
end
end
end