lib/chef/knife/openstack_server_create.rb in knife-openstack-0.5.4 vs lib/chef/knife/openstack_server_create.rb in knife-openstack-0.6.0

- old
+ new

@@ -1,8 +1,9 @@ # # Author:: Seth Chisamore (<schisamo@opscode.com>) -# Copyright:: Copyright (c) 2011 Opscode, Inc. +# Author:: Matt Ray (<matt@opscode.com>) +# Copyright:: Copyright (c) 2011-2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # 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 @@ -14,138 +15,123 @@ # 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 'chef/knife' +require 'chef/knife/openstack_base' class Chef class Knife class OpenstackServerCreate < Knife + include Knife::OpenstackBase + deps do - require 'chef/knife/bootstrap' - Chef::Knife::Bootstrap.load_deps require 'fog' - require 'socket' - require 'net/ssh/multi' require 'readline' require 'chef/json_compat' + require 'chef/knife/bootstrap' + Chef::Knife::Bootstrap.load_deps end banner "knife openstack server create (options)" attr_accessor :initial_sleep_delay option :flavor, - :short => "-f FLAVOR", - :long => "--flavor FLAVOR", - :description => "The flavor of server (m1.small, m1.medium, etc)", - :proc => Proc.new { |f| Chef::Config[:knife][:flavor] = f } + :short => "-f FLAVOR_ID", + :long => "--flavor FLAVOR_ID", + :description => "The flavor ID of server (m1.small, m1.medium, etc)", + :proc => Proc.new { |f| Chef::Config[:knife][:flavor] = f } option :image, - :short => "-I IMAGE", - :long => "--image IMAGE", - :description => "The AMI for the server", - :proc => Proc.new { |i| Chef::Config[:knife][:image] = i } + :short => "-I IMAGE_ID", + :long => "--image IMAGE_ID", + :description => "The image ID for the server", + :proc => Proc.new { |i| Chef::Config[:knife][:image] = i } - option :security_groups, - :short => "-G X,Y,Z", - :long => "--groups X,Y,Z", - :description => "The security groups for this server", - :default => ["default"], - :proc => Proc.new { |groups| groups.split(',') } + # option :security_groups, + # :short => "-G X,Y,Z", + # :long => "--groups X,Y,Z", + # :description => "The security groups for this server", + # :default => ["default"], + # :proc => Proc.new { |groups| groups.split(',') } - option :availability_zone, - :short => "-Z ZONE", - :long => "--availability-zone ZONE", - :description => "The Availability Zone", - :proc => Proc.new { |key| Chef::Config[:knife][:availability_zone] = key } - option :chef_node_name, - :short => "-N NAME", - :long => "--node-name NAME", - :description => "The Chef node name for your new node" + :short => "-N NAME", + :long => "--node-name NAME", + :description => "The Chef node name for your new node" + option :floating_ip, + :short => "-a", + :long => "--floating-ip", + :boolean => true, + :default => false, + :description => "Request to associate a floating IP address to the new OpenStack node. Assumes IPs have been allocated to the project." + + option :private_network, + :long => "--private-network", + :description => "Use the private IP for bootstrapping rather than the public IP", + :boolean => true, + :default => false + option :ssh_key_name, - :short => "-S KEY", - :long => "--ssh-key KEY", - :description => "The OpenStack SSH key id", - :proc => Proc.new { |key| Chef::Config[:knife][:openstack_ssh_key_id] = key } + :short => "-S KEY", + :long => "--ssh-key KEY", + :description => "The OpenStack SSH keypair id", + :proc => Proc.new { |key| Chef::Config[:knife][:openstack_ssh_key_id] = key } option :ssh_user, - :short => "-x USERNAME", - :long => "--ssh-user USERNAME", - :description => "The ssh username", - :default => "root" + :short => "-x USERNAME", + :long => "--ssh-user USERNAME", + :description => "The ssh username", + :default => "root" option :ssh_password, - :short => "-P PASSWORD", - :long => "--ssh-password PASSWORD", - :description => "The ssh password" + :short => "-P PASSWORD", + :long => "--ssh-password PASSWORD", + :description => "The ssh password" option :identity_file, - :short => "-i IDENTITY_FILE", - :long => "--identity-file IDENTITY_FILE", - :description => "The SSH identity file used for authentication" + :short => "-i IDENTITY_FILE", + :long => "--identity-file IDENTITY_FILE", + :description => "The SSH identity file used for authentication" - option :openstack_access_key_id, - :short => "-A ID", - :long => "--openstack-access-key-id KEY", - :description => "Your OpenStack Access Key ID", - :proc => Proc.new { |key| Chef::Config[:knife][:openstack_access_key_id] = key } - - option :openstack_secret_access_key, - :short => "-K SECRET", - :long => "--openstack-secret-access-key SECRET", - :description => "Your OpenStack API Secret Access Key", - :proc => Proc.new { |key| Chef::Config[:knife][:openstack_secret_access_key] = key } - - option :openstack_api_endpoint, - :long => "--openstack-api-endpoint ENDPOINT", - :description => "Your OpenStack API endpoint", - :proc => Proc.new { |endpoint| Chef::Config[:knife][:openstack_api_endpoint] = endpoint } - option :prerelease, - :long => "--prerelease", - :description => "Install the pre-release chef gems" + :long => "--prerelease", + :description => "Install the pre-release chef gems" option :bootstrap_version, - :long => "--bootstrap-version VERSION", - :description => "The version of Chef to install", - :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v } + :long => "--bootstrap-version VERSION", + :description => "The version of Chef to install", + :proc => Proc.new { |v| Chef::Config[:knife][:bootstrap_version] = v } - option :region, - :long => "--region REGION", - :description => "Your OpenStack region", - :proc => Proc.new { |region| Chef::Config[:knife][:region] = region } - option :distro, - :short => "-d DISTRO", - :long => "--distro DISTRO", - :description => "Bootstrap a distro using a template", - :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d }, - :default => "ubuntu10.04-gems" + :short => "-d DISTRO", + :long => "--distro DISTRO", + :description => "Bootstrap a distro using a template; default is 'chef-full'", + :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d }, + :default => "chef-full" 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 + :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 :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 => [] + :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 :no_host_key_verify, - :long => "--no-host-key-verify", - :description => "Disable host key verification", - :boolean => true, - :default => false + option :host_key_verify, + :long => "--[no-]host-key-verify", + :description => "Verify host key, enabled by default", + :boolean => true, + :default => true def tcp_test_ssh(hostname) tcp_socket = TCPSocket.new(hostname, 22) readable = IO.select([tcp_socket], nil, nil, 5) if readable @@ -155,105 +141,165 @@ else false end rescue Errno::ETIMEDOUT false + rescue Errno::EPERM + false rescue Errno::ECONNREFUSED sleep 2 false + rescue Errno::EHOSTUNREACH + sleep 2 + false + rescue Errno::ENETUNREACH + sleep 2 + false ensure tcp_socket && tcp_socket.close end def run - $stdout.sync = true + validate! + connection = Fog::Compute.new( - :provider => 'AWS', - :aws_access_key_id => Chef::Config[:knife][:openstack_access_key_id], - :aws_secret_access_key => Chef::Config[:knife][:openstack_secret_access_key], - :endpoint => Chef::Config[:knife][:openstack_api_endpoint], - :region => locate_config_value(:region) - ) + :provider => 'OpenStack', + :openstack_username => Chef::Config[:knife][:openstack_username], + :openstack_api_key => Chef::Config[:knife][:openstack_password], + :openstack_auth_url => Chef::Config[:knife][:openstack_auth_url], + :openstack_tenant => Chef::Config[:knife][:openstack_tenant] + ) + #servers require a name, generate one if not passed + node_name = get_node_name(config[:chef_node_name]) + server_def = { - :image_id => locate_config_value(:image), - :groups => config[:security_groups], - :flavor_id => locate_config_value(:flavor), - :key_name => Chef::Config[:knife][:openstack_ssh_key_id], - :availability_zone => Chef::Config[:knife][:availability_zone] - } + :name => node_name, + :image_ref => locate_config_value(:image), + :flavor_ref => locate_config_value(:flavor), + # :security_group => locate_config_value(:security_groups), + :key_name => Chef::Config[:knife][:openstack_ssh_key_id], + :personality => [{ + "path" => "/etc/chef/ohai/hints/openstack.json", + "contents" => '' + }] + } - server = connection.servers.create(server_def) + Chef::Log.debug("Name #{node_name}") + Chef::Log.debug("Image #{locate_config_value(:image)}") + Chef::Log.debug("Flavor #{locate_config_value(:flavor)}") + # Chef::Log.debug("Groups #{locate_config_value(:security_groups)}") + Chef::Log.debug("Creating server #{server_def}") + server = connection.servers.create(server_def) - puts "#{ui.color("Instance ID", :cyan)}: #{server.id}" - puts "#{ui.color("Flavor", :cyan)}: #{server.flavor_id}" - puts "#{ui.color("Image", :cyan)}: #{server.image_id}" - puts "#{ui.color("Availability Zone", :cyan)}: #{server.availability_zone}" - puts "#{ui.color("Security Groups", :cyan)}: #{server.groups.join(", ")}" - puts "#{ui.color("SSH Key", :cyan)}: #{server.key_name}" + msg_pair("Instance Name", server.name) + msg_pair("Instance ID", server.id) + # msg_pair("Security Groups", server.groups.join(", ")) + msg_pair("SSH Keypair", server.key_name) - print "\n#{ui.color("Waiting for server", :magenta)}" + print "\n#{ui.color("Waiting for server", :magenta)}" - display_name = server.dns_name + # wait for it to be ready to do stuff + server.wait_for { print "."; ready? } - # wait for it to be ready to do stuff - server.wait_for { print "."; ready? } + puts("\n") - puts("\n") + msg_pair("Flavor", server.flavor['id']) + msg_pair("Image", server.image['id']) + msg_pair("Public IP Address", server.public_ip_address['addr']) if server.public_ip_address - puts "#{ui.color("Public DNS Name", :cyan)}: #{server.dns_name}" - puts "#{ui.color("Public IP Address", :cyan)}: #{server.public_ip_address}" - puts "#{ui.color("Private DNS Name", :cyan)}: #{server.private_dns_name}" - puts "#{ui.color("Private IP Address", :cyan)}: #{server.private_ip_address}" + if config[:floating_ip] + associated = false + connection.addresses.each do |address| + if address.instance_id.nil? + server.associate_address(address.ip) + #a bit of a hack, but server.reload takes a long time + server.addresses['public'].push({"version"=>4,"addr"=>address.ip}) + associated = true + msg_pair("Floating IP Address", address.ip) + break + end + end + unless associated + ui.error("Unable to associate floating IP.") + exit 1 + end + end + Chef::Log.debug("Public IP Address actual #{server.public_ip_address['addr']}") if server.public_ip_address - print "\n#{ui.color("Waiting for sshd", :magenta)}" + msg_pair("Private IP Address", server.private_ip_address['addr']) - print(".") until tcp_test_ssh(display_name) { - sleep @initial_sleep_delay ||= 10 - puts("done") - } + #which IP address to bootstrap + bootstrap_ip_address = server.public_ip_address['addr'] if server.public_ip_address + if config[:private_network] + bootstrap_ip_address = server.private_ip_address['addr'] + end + Chef::Log.debug("Bootstrap IP Address #{bootstrap_ip_address}") + if bootstrap_ip_address.nil? + ui.error("No IP address available for bootstrapping.") + exit 1 + end - bootstrap_for_node(server).run + print "\n#{ui.color("Waiting for sshd", :magenta)}" - puts "\n" - puts "#{ui.color("Instance ID", :cyan)}: #{server.id}" - puts "#{ui.color("Flavor", :cyan)}: #{server.flavor_id}" - puts "#{ui.color("Image", :cyan)}: #{server.image_id}" - puts "#{ui.color("Availability Zone", :cyan)}: #{server.availability_zone}" - puts "#{ui.color("Security Groups", :cyan)}: #{server.groups.join(", ")}" - puts "#{ui.color("Public DNS Name", :cyan)}: #{server.dns_name}" - puts "#{ui.color("Public IP Address", :cyan)}: #{server.public_ip_address}" - puts "#{ui.color("Private DNS Name", :cyan)}: #{server.private_dns_name}" - puts "#{ui.color("SSH Key", :cyan)}: #{server.key_name}" - puts "#{ui.color("Private IP Address", :cyan)}: #{server.private_ip_address}" - puts "#{ui.color("Environment", :cyan)}: #{config[:environment] || '_default'}" - puts "#{ui.color("Run List", :cyan)}: #{config[:run_list].join(', ')}" - end + print(".") until tcp_test_ssh(bootstrap_ip_address) { + sleep @initial_sleep_delay ||= 10 + puts("done") + } - def bootstrap_for_node(server) - bootstrap = Chef::Knife::Bootstrap.new - bootstrap.name_args = [server.dns_name] - bootstrap.config[:run_list] = config[:run_list] - bootstrap.config[:ssh_user] = config[:ssh_user] - bootstrap.config[:identity_file] = config[:identity_file] - bootstrap.config[:chef_node_name] = config[:chef_node_name] || server.id - bootstrap.config[:prerelease] = config[:prerelease] - bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version) - bootstrap.config[:distro] = locate_config_value(:distro) - bootstrap.config[:use_sudo] = true - bootstrap.config[:template_file] = locate_config_value(:template_file) - bootstrap.config[:environment] = config[:environment] - # may be needed for vpc_mode - bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify] - bootstrap - end + bootstrap_for_node(server, bootstrap_ip_address).run - def locate_config_value(key) - key = key.to_sym - Chef::Config[:knife][key] || config[key] + puts "\n" + msg_pair("Instance Name", server.name) + msg_pair("Instance ID", server.id) + msg_pair("Flavor", server.flavor['id']) + msg_pair("Image", server.image['id']) + # msg_pair("Security Groups", server.groups.join(", ")) + msg_pair("SSH Keypair", server.key_name) + msg_pair("Public IP Address", server.public_ip_address['addr']) if server.public_ip_address + msg_pair("Private IP Address", server.private_ip_address['addr']) + msg_pair("Environment", config[:environment] || '_default') + msg_pair("Run List", config[:run_list].join(', ')) + end + + def bootstrap_for_node(server, bootstrap_ip_address) + bootstrap = Chef::Knife::Bootstrap.new + bootstrap.name_args = [bootstrap_ip_address] + bootstrap.config[:run_list] = config[:run_list] + bootstrap.config[:ssh_user] = config[:ssh_user] + bootstrap.config[:identity_file] = config[:identity_file] + bootstrap.config[:host_key_verify] = config[:host_key_verify] + bootstrap.config[:chef_node_name] = server.name + bootstrap.config[:prerelease] = config[:prerelease] + bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version) + bootstrap.config[:distro] = locate_config_value(:distro) + bootstrap.config[:use_sudo] = true unless config[:ssh_user] == 'root' + bootstrap.config[:template_file] = locate_config_value(:template_file) + bootstrap.config[:environment] = config[:environment] + bootstrap + end + + def ami + @ami ||= connection.images.get(locate_config_value(:image)) + end + + def validate! + + super([:image, :openstack_ssh_key_id, :openstack_username, :openstack_password, :openstack_auth_url]) + + if ami.nil? + ui.error("You have not provided a valid image ID. Please note the short option for this value recently changed from '-i' to '-I'.") + exit 1 end + end + #generate a random name if chef_node_name is empty + def get_node_name(chef_node_name) + return chef_node_name unless chef_node_name.nil? + #lazy uuids + chef_node_name = "os-"+rand.to_s.split('.')[1] end end +end end