Merge pull request #25 from opscode/OC-7465

Oc 7465 - Add support to create Linux VMs with ssh keys
This commit is contained in:
Chirag Jog 2013-05-23 23:40:04 -07:00
Родитель 064db97989 6a9f8b9920
Коммит d144be6d2b
6 изменённых файлов: 150 добавлений и 12 удалений

87
lib/azure/certificate.rb Executable file
Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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)

Просмотреть файл

@ -177,8 +177,20 @@ class Azure
xml.ConfigurationSetType 'LinuxProvisioningConfiguration'
xml.HostName params[:host_name]
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') {

Просмотреть файл

@ -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, '')
@ -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

Просмотреть файл

@ -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