Merge pull request #25 from opscode/OC-7465
Oc 7465 - Add support to create Linux VMs with ssh keys
This commit is contained in:
Коммит
d144be6d2b
|
@ -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)
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче