diff --git a/lib/azure/certificate.rb b/lib/azure/certificate.rb new file mode 100755 index 0000000..6235cd8 --- /dev/null +++ b/lib/azure/certificate.rb @@ -0,0 +1,87 @@ +# +# Author:: Mukta Aphale (mukta.aphale@clogeny.com) +# Copyright:: Copyright (c) 2010-2011 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 +# +# 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. +# + +class Azure + class Certificates + def initialize(connection) + @connection=connection + end + def create(params) + certificate = Certificate.new(@connection) + certificate.create(params) + end + end +end + +class Azure + class Certificate + attr_accessor :connection + attr_accessor :cert_data, :fingerprint, :certificate_version + def initialize(connection) + @connection = connection + @certificate_version = 2 # cf. RFC 5280 - to make it a "v3" certificate + end + def create(params) + # If RSA private key has been specified, then generate an x 509 certificate from the + # public part of the key + @cert_data = generate_public_key_certificate_data({:ssh_key => params[:identity_file], + :ssh_key_passphrase => params[:identity_file_passphrase]}) + # Generate XML to call the API + # Add certificate to the hosted service + builder = Nokogiri::XML::Builder.new do |xml| + xml.CertificateFile('xmlns'=>'http://schemas.microsoft.com/windowsazure') { + xml.Data @cert_data + xml.CertificateFormat 'pfx' + xml.Password 'knifeazure' + } + end + # Windows Azure API call + @connection.query_azure("hostedservices/#{params[:hosted_service_name]}/certificates", "post", builder.to_xml) + # Return the fingerprint to be used while adding role + @fingerprint + end + + def generate_public_key_certificate_data (params) + # Generate OpenSSL RSA key from the mentioned ssh key path (and passphrase) + key = OpenSSL::PKey::RSA.new(File.read(params[:ssh_key]), params[:ssh_key_passphrase]) + # Generate X 509 certificate + ca = OpenSSL::X509::Certificate.new + ca.version = @certificate_version + ca.serial = Random.rand(65534) + 1 # 2 digit byte range random number for better security aspect + ca.subject = OpenSSL::X509::Name.parse "/DC=org/DC=knife-plugin/CN=Opscode CA" + ca.issuer = ca.subject # root CA's are "self-signed" + ca.public_key = key.public_key # Assign the ssh-key's public part to the certificate + ca.not_before = Time.now + ca.not_after = ca.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = ca + ef.issuer_certificate = ca + ca.add_extension(ef.create_extension("basicConstraints","CA:TRUE",true)) + ca.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true)) + ca.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false)) + ca.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false)) + ca.sign(key, OpenSSL::Digest::SHA256.new) + # Generate the SHA1 fingerprint of the der format of the X 509 certificate + @fingerprint = OpenSSL::Digest::SHA1.new(ca.to_der) + # Create the pfx format of the certificate + pfx = OpenSSL::PKCS12.create('knifeazure', 'knife-azure-pfx', key, ca) + # Encode the pfx format - upload this certificate + Base64.strict_encode64(pfx.to_der) + end + end +end diff --git a/lib/azure/connection.rb b/lib/azure/connection.rb index 060636a..e77a9dc 100755 --- a/lib/azure/connection.rb +++ b/lib/azure/connection.rb @@ -23,11 +23,12 @@ require File.expand_path('../deploy', __FILE__) require File.expand_path('../role', __FILE__) require File.expand_path('../disk', __FILE__) require File.expand_path('../image', __FILE__) +require File.expand_path('../certificate', __FILE__) class Azure class Connection include AzureAPI - attr_accessor :hosts, :rest, :images, :deploys, :roles, :disks, :storageaccounts + attr_accessor :hosts, :rest, :images, :deploys, :roles, :disks, :storageaccounts, :certificates def initialize(params={}) @rest = Rest.new(params) @hosts = Hosts.new(self) @@ -36,6 +37,7 @@ class Azure @deploys = Deploys.new(self) @roles = Roles.new(self) @disks = Disks.new(self) + @certificates = Certificates.new(self) end def query_azure(service_name, verb = 'get', body = '') Chef::Log.info 'calling ' + verb + ' ' + service_name diff --git a/lib/azure/deploy.rb b/lib/azure/deploy.rb index cd645be..a2d0dce 100755 --- a/lib/azure/deploy.rb +++ b/lib/azure/deploy.rb @@ -48,6 +48,9 @@ class Azure unless @connection.storageaccounts.exists(params[:storage_account]) @connection.storageaccounts.create(params) end + if params[:identity_file] + params[:fingerprint] = @connection.certificates.create(params) + end params['deploy_name'] = find(params[:hosted_service_name]) if params['deploy_name'] != nil role = Role.new(@connection) diff --git a/lib/azure/role.rb b/lib/azure/role.rb index 1d884c1..0bfe89d 100755 --- a/lib/azure/role.rb +++ b/lib/azure/role.rb @@ -176,9 +176,21 @@ class Azure xml.ConfigurationSet('i:type' => 'LinuxProvisioningConfigurationSet') { xml.ConfigurationSetType 'LinuxProvisioningConfiguration' xml.HostName params[:host_name] - xml.UserName params[:ssh_user] - xml.UserPassword params[:ssh_password] - xml.DisableSshPasswordAuthentication 'false' + xml.UserName params[:ssh_user] + unless params[:identity_file].nil? + xml.DisableSshPasswordAuthentication 'true' + xml.SSH { + xml.PublicKeys { + xml.PublicKey { + xml.Fingerprint params[:fingerprint] + xml.Path '/home/' + params[:ssh_user] + '/.ssh/authorized_keys' + } + } + } + else + xml.UserPassword params[:ssh_password] + xml.DisableSshPasswordAuthentication 'false' + end } elsif params[:os_type] == 'Windows' xml.ConfigurationSet('i:type' => 'WindowsProvisioningConfigurationSet') { diff --git a/lib/chef/knife/azure_server_create.rb b/lib/chef/knife/azure_server_create.rb index 2ce2e4d..45b0f9c 100755 --- a/lib/chef/knife/azure_server_create.rb +++ b/lib/chef/knife/azure_server_create.rb @@ -67,11 +67,6 @@ class Chef :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" - option :prerelease, :long => "--prerelease", :description => "Install the pre-release chef gems" @@ -162,6 +157,13 @@ class Chef :long => "--udp-endpoints PORT_LIST", :description => "Comma separated list of UDP local and public ports to open i.e. '80:80,433:5000'" + option :identity_file, + :long => "--identity-file FILENAME", + :description => "SSH key path, optional. It is the RSA private key. Specify either ssh-password or identity-file" + + option :identity_file_passphrase, + :long => "--identity-file-passphrase PASSWORD", + :description => "SSH key passphrase. Optional, specify if passphrase for identity-file exists" def strip_non_ascii(string) string.gsub(/[^0-9a-z ]/i, '') @@ -408,7 +410,7 @@ class Chef :role_size => locate_config_value(:role_size), :tcp_endpoints => locate_config_value(:tcp_endpoints), :udp_endpoints => locate_config_value(:udp_endpoints), - :bootstrap_proto => locate_config_value(:bootstrap_protocol) + :bootstrap_proto => locate_config_value(:bootstrap_protocol) } if is_image_windows? @@ -421,12 +423,19 @@ class Chef else server_def[:os_type] = 'Linux' server_def[:bootstrap_proto] = 'ssh' - if not locate_config_value(:ssh_user) or not locate_config_value(:ssh_password) - ui.error("SSH User and SSH Password are compulsory parameters") + if not locate_config_value(:ssh_user) + ui.error("SSH User is compulsory parameter") exit 1 end + unless locate_config_value(:ssh_password) or locate_config_value(:identity_file) + ui.error("Specify either SSH Key or SSH Password") + exit 1 + end + server_def[:ssh_user] = locate_config_value(:ssh_user) server_def[:ssh_password] = locate_config_value(:ssh_password) + server_def[:identity_file] = locate_config_value(:identity_file) + server_def[:identity_file_passphrase] = locate_config_value(:identity_file_passphrase) end server_def end diff --git a/spec/unit/azure_server_create_spec.rb b/spec/unit/azure_server_create_spec.rb index b9da6b8..f49b2bd 100644 --- a/spec/unit/azure_server_create_spec.rb +++ b/spec/unit/azure_server_create_spec.rb @@ -169,6 +169,31 @@ describe "for bootstrap protocol ssh:" do @server_instance.run end + context "ssh key" do + before do + Chef::Config[:knife][:ssh_password] = '' + Chef::Config[:knife][:identity_file] = 'ssh_key' + end + it "check if ssh-key set correctly" do + @server_instance.should_receive(:is_image_windows?).and_return(false) + @server_params = @server_instance.create_server_def + @server_params[:os_type].should == 'Linux' + @server_params[:identity_file].should == 'ssh_key' + @server_params[:ssh_user].should == 'ssh_user' + @server_params[:bootstrap_proto].should == 'ssh' + @server_params[:hosted_service_name].should == 'service001' + end + it "successful bootstrap with ssh key" do + @server_instance.should_receive(:is_image_windows?).exactly(3).times.and_return(false) + @bootstrap = Chef::Knife::Bootstrap.new + Chef::Knife::Bootstrap.stub(:new).and_return(@bootstrap) + @bootstrap.should_receive(:run) + @server_instance.connection.certificates.stub(:generate_public_key_certificate_data).and_return("cert_data") + @server_instance.connection.certificates.should_receive(:create) + @server_instance.run + end + end + context "bootstrap" before do @server_params = @server_instance.create_server_def