lib/chef/knife/google_server_create.rb in knife-google-1.1.0 vs lib/chef/knife/google_server_create.rb in knife-google-1.2.0

- old
+ new

@@ -10,10 +10,11 @@ # 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 'timeout' require 'chef/knife/google_base' class Chef class Knife class GoogleServerCreate < Knife @@ -32,51 +33,93 @@ attr_accessor :initial_sleep_delay attr_reader :instance option :machine_type, :short => "-m MACHINE_TYPE", - :long => "--google-compute-machine MACHINE_TYPE", + :long => "--gce-machine-type MACHINE_TYPE", :description => "The machine type of server (n1-highcpu-2, n1-highcpu-2-d, etc)", :required => true option :image, :short => "-I IMAGE", - :long => "--google-compute-image IMAGE", + :long => "--gce-image IMAGE", :description => "The Image for the server", :required => true option :image_project_id, - :short => "-J IMAGE_PROJECT_ID", - :long => "--google-compute-image-project-id IMAGE_PROJECT_ID", + :long => "--gce-image-project-id IMAGE_PROJECT_ID", :description => "The project-id containing the Image (debian-cloud, centos-cloud, etc)", :default => "" option :zone, :short => "-Z ZONE", - :long => "--google-compute-zone ZONE", + :long => "--gce-zone ZONE", :description => "The Zone for this server" + option :boot_disk_name, + :long => "--gce-boot-disk-name DISK", + :description => "Name of persistent boot disk; default is to use the server name", + :default => "" + + option :boot_disk_size, + :long => "--gce-boot-disk-size SIZE", + :description => "Size of the persistent boot disk between 10 and 10000 GB, specified in GB; default is '10' GB", + :default => "10" + + option :auto_restart, + :long => "--[no-]gce-auto-server-restart", + :description => "Compute Engine can automatically restart your VM instance if it is terminated for non-user-initiated reasons; enabled by default.", + :boolean => true, + :default => true + + option :auto_migrate, + :long => "--[no-]gce-auto-server-migrate", + :description => "Compute Engine can migrate your VM instance to other hardware without downtime prior to periodic infrastructure maintenance, otherwise the server is terminated; enabled by default.", + :boolean => true, + :default => true + option :network, :short => "-n NETWORK", - :long => "--google-compute-network NETWORK", + :long => "--gce-network NETWORK", :description => "The network for this server; default is 'default'", :default => "default" option :tags, :short => "-T TAG1,TAG2,TAG3", - :long => "--google-compute-tags TAG1,TAG2,TAG3", + :long => "--gce-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", + :long => "--gce-metadata Key=Value[,Key=Value...]", + :description => "Additional metadata for this server", :proc => Proc.new { |metadata| metadata.split(',') }, :default => [] + option :service_account_scopes, + :long => "--gce-service-account-scopes SCOPE1,SCOPE2,SCOPE3", + :proc => Proc.new { |service_account_scopes| service_account_scopes.split(',') }, + :description => "Service account scopes for this server", + :default => [] + + # GCE documentation uses the term 'service account name', the api uses the term 'email' + option :service_account_name, + :long => "--gce-service-account-name NAME", + :description => "Service account name for this server, typically in the form of '123845678986@project.gserviceaccount.com'; default is 'default'", + :default => "default" + + option :instance_connect_ip, + :long => "--gce-server-connect-ip INTERFACE", + :description => "Whether to use PUBLIC or PRIVATE interface/address to connect; default is 'PUBLIC'", + :default => 'PUBLIC' + + option :public_ip, + :long=> "--gce-public-ip IP_ADDRESS", + :description => "EPHEMERAL or static IP address or NONE; default is 'EPHEMERAL'", + :default => "EPHEMERAL" + option :chef_node_name, :short => "-N NAME", :long => "--node-name NAME", :description => "The Chef node name for your new node" @@ -156,27 +199,10 @@ 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}") @@ -198,11 +224,11 @@ config[:ssh_gateway] ? wait_for_tunnelled_sshd(hostname) : wait_for_direct_sshd(hostname, config[:ssh_port]) end def wait_for_tunnelled_sshd(hostname) print(".") - print(".") until tunnel_test_ssh(ssh_connect_host) { + print(".") until tunnel_test_ssh(hostname) { sleep @initial_sleep_delay ||= 40 puts("done") } end @@ -235,11 +261,39 @@ else private_ips(@instance).first end end - def bootstrap_for_node(instance,ssh_host) + def disk_exists(disk, zone) + # if client.disks.get errors with a Google::Compute::ResourceNotFound + # then the disk does not exist and can be created + client.disks.get(:disk => disk, :zone => selflink2name(zone)) + rescue Google::Compute::ResourceNotFound + # disk does not exist + # continue provisioning + false + else + true + end + + def wait_for_disk(disk, operation, zone) + Timeout::timeout(300) do + until disk.status == 'DONE' + ui.info(".") + sleep 1 + disk = client.zoneOperations.get(:name => disk, + :operation => operation, + :zone => selflink2name(zone)) + end + disk.target_link + end + rescue Timeout::Error + ui.error("Timeout exceeded with disk status: " + disk.status) + exit 1 + end + + 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] @@ -256,44 +310,60 @@ # 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]["gce"] ||= {} Chef::Config[:knife][:hints]["google"] ||= {} bootstrap end def run $stdout.sync = true unless @name_args.size > 0 - ui.error("Please provide the name of the new server") + ui.error("Please provide the name of the new server.") exit 1 end begin - zone = client.zones.get(config[:zone] || Chef::Config[:knife][:google_compute_zone]).self_link + zone = client.zones.get(config[:zone] || Chef::Config[:knife][:gce_zone]).self_link rescue Google::Compute::ResourceNotFound - ui.error("Zone '#{config[:zone] || Chef::Config[:knife][:google_compute_zone]}' not found") + ui.error("Zone '#{config[:zone] || Chef::Config[:knife][:gce_zone]}' not found.") exit 1 rescue Google::Compute::ParameterValidation ui.error("Must specify zone in knife config file or in command line as an option. Try --help.") exit 1 end begin - machine_type = client.machine_types.get(:name=>config[:machine_type], :zone=>selflink2name(zone)).self_link + machine_type = client.machine_types.get(:name => config[:machine_type], + :zone => selflink2name(zone)).self_link rescue Google::Compute::ResourceNotFound ui.error("MachineType '#{config[:machine_type]}' not found") exit 1 end + # this parameter is a string during the post and boolean otherwise + if config[:auto_restart] then + auto_restart = 'true' + else + auto_restart = 'false' + end + + if config[:auto_migrate] then + auto_migrate = 'MIGRATE' + else + auto_migrate = 'TERMINATE' + end + (checked_custom, checked_all) = false begin image_project = config[:image_project_id] - machine_type=~Regexp.new('/projects/(.*?)/') + # use zone url to determine project name + zone =~ Regexp.new('/projects/(.*?)/') project = $1 - if image_project.empty? + if image_project.to_s.empty? unless checked_custom checked_custom = true ui.info("Looking for Image '#{config[:image]}' in Project '#{project}'") image = client.images.get(:project=>project, :name=>config[:image]).self_link else @@ -322,17 +392,37 @@ exit 1 end end begin + boot_disk_size = config[:boot_disk_size].to_i + raise if !boot_disk_size.between?(10, 10000) + rescue + ui.error("Size of the persistent boot disk must be between 10 and 10000 GB.") + exit 1 + end + + if config[:boot_disk_name].to_s.empty? then + boot_disk_name = @name_args.first + else + boot_disk_name = config[:boot_disk_name] + end + + ui.info("Waiting for the disk insert operation to complete") + boot_disk_insert = client.disks.insert(:sourceImage => image, + :zone => selflink2name(zone), + :name => boot_disk_name, + :sizeGb => boot_disk_size) + boot_disk_target_link = wait_for_disk(boot_disk_insert, boot_disk_insert.name, zone) + + begin network = client.networks.get(config[:network]).self_link rescue Google::Compute::ResourceNotFound ui.error("Network '#{config[:network]}' not found") exit 1 end - 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", @@ -344,30 +434,68 @@ # 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] - ) ui.info("Waiting for the create server operation to complete") + if !config[:service_account_scopes].any? + zone_operation = client.instances.create(:name => @name_args.first, + :zone => selflink2name(zone), + :machineType => machine_type, + :disks => [{ + 'boot' => true, + 'type' => 'PERSISTENT', + 'mode' => 'READ_WRITE', + 'deviceName' => selflink2name(boot_disk_target_link), + 'source' => boot_disk_target_link + }], + :networkInterfaces => [network_interface], + :scheduling => { + 'automaticRestart' => auto_restart, + 'onHostMaintenance' => auto_migrate + }, + :metadata => { 'items' => metadata }, + :tags => { 'items' => config[:tags] } + ) + else + zone_operation = client.instances.create(:name => @name_args.first, + :zone=> selflink2name(zone), + :machineType => machine_type, + :disks => [{ + 'boot' => true, + 'type' => 'PERSISTENT', + 'mode' => 'READ_WRITE', + 'deviceName' => selflink2name(boot_disk_target_link), + 'source' => boot_disk_target_link + }], + :networkInterfaces => [network_interface], + :serviceAccounts => [{ + 'kind' => 'compute#serviceAccount', + 'email' => config[:service_account_name], + 'scopes' => config[:service_account_scopes] + }], + :scheduling => { + 'automaticRestart' => auto_restart, + 'onHostMaintenance' => auto_migrate + }, + :metadata => { 'items'=>metadata }, + :tags => { 'items' => config[:tags] } + ) + end + 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") @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("Machine Type", selflink2name(@instance.machine_type)) + msg_pair("Image", selflink2name(config[: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)