commit 3e5a887b0756659c757c6f9ffca8e87567b85fca Author: Barry Steinglass Date: Wed Jun 6 11:12:35 2012 -0700 initial deliverable from the contractor. diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f9f60d0 Binary files /dev/null and b/.DS_Store differ diff --git a/Gemfile b/Gemfile new file mode 100755 index 0000000..25d9112 --- /dev/null +++ b/Gemfile @@ -0,0 +1,23 @@ +source "http://rubygems.org" +# Add dependencies required to use your gem here. +# Example: +# gem "activesupport", ">= 2.3.5" + +gemspec + #gem "chef", "~> 0.10.8" + +# Add dependencies to develop your gem here. +# Include everything needed to run rake, tests, features, etc. +group :development do + gem "rspec", "~> 2.8.0" + gem "rdoc", "~> 3.12" + gem "bundler", "~> 1.0.0" + gem "jeweler", "~> 1.8.3" + gem "rcov", ">= 0" + gem "guard-rspec" + gem "libnotify" + gem "rubygems-bundler", "~> 0.2.8" + gem "interactive_editor" + gem "equivalent-xml", "~> 0.2.9" +end + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100755 index 0000000..1c964ea --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,110 @@ +PATH + remote: . + specs: + knife-azure (0.5.11) + chef (~> 0.10) + nokogiri + +GEM + remote: http://rubygems.org/ + specs: + bunny (0.7.9) + chef (0.10.8) + bunny (>= 0.6.0) + erubis + highline + json (>= 1.4.4, <= 1.6.1) + mixlib-authentication (>= 1.1.0) + mixlib-cli (>= 1.1.0) + mixlib-config (>= 1.1.2) + mixlib-log (>= 1.3.0) + moneta + net-ssh (~> 2.1.3) + net-ssh-multi (~> 1.1.0) + ohai (>= 0.6.0) + rest-client (>= 1.0.4, < 1.7.0) + treetop (~> 1.4.9) + uuidtools + diff-lcs (1.1.3) + equivalent-xml (0.2.9) + nokogiri (>= 1.4.3) + erubis (2.7.0) + ffi (1.0.11) + git (1.2.5) + guard (1.0.1) + ffi (>= 0.5.0) + thor (~> 0.14.6) + guard-rspec (0.7.0) + guard (>= 0.10.0) + highline (1.6.11) + interactive_editor (0.0.10) + spoon (>= 0.0.1) + ipaddress (0.8.0) + jeweler (1.8.3) + bundler (~> 1.0) + git (>= 1.2.5) + rake + rdoc + json (1.6.1) + libnotify (0.7.2) + mime-types (1.18) + mixlib-authentication (1.1.4) + mixlib-log + mixlib-cli (1.2.2) + mixlib-config (1.1.2) + mixlib-log (1.3.0) + moneta (0.6.0) + net-ssh (2.1.4) + net-ssh-gateway (1.1.0) + net-ssh (>= 1.99.1) + net-ssh-multi (1.1) + net-ssh (>= 2.1.4) + net-ssh-gateway (>= 0.99.0) + nokogiri (1.5.0) + ohai (0.6.12) + ipaddress + mixlib-cli + mixlib-config + mixlib-log + systemu + yajl-ruby + polyglot (0.3.3) + rake (0.9.2.2) + rcov (1.0.0) + rdoc (3.12) + json (~> 1.4) + rest-client (1.6.7) + mime-types (>= 1.16) + rspec (2.8.0) + rspec-core (~> 2.8.0) + rspec-expectations (~> 2.8.0) + rspec-mocks (~> 2.8.0) + rspec-core (2.8.0) + rspec-expectations (2.8.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.8.0) + rubygems-bundler (0.2.8) + spoon (0.0.1) + systemu (2.5.0) + thor (0.14.6) + treetop (1.4.10) + polyglot + polyglot (>= 0.3.1) + uuidtools (2.1.2) + yajl-ruby (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.0.0) + equivalent-xml (~> 0.2.9) + guard-rspec + interactive_editor + jeweler (~> 1.8.3) + knife-azure! + libnotify + rcov + rdoc (~> 3.12) + rspec (~> 2.8.0) + rubygems-bundler (~> 0.2.8) diff --git a/Guardfile b/Guardfile new file mode 100755 index 0000000..730d772 --- /dev/null +++ b/Guardfile @@ -0,0 +1,8 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +guard :rspec, :version => 2 do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) # { |m| "spec/lib/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec" } +end diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000..19e9fc6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2012 Barry Davis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Rakefile b/Rakefile new file mode 100755 index 0000000..b80d2a8 --- /dev/null +++ b/Rakefile @@ -0,0 +1,57 @@ +# encoding: utf-8 + +require 'rubygems' +require 'bundler' +begin + Bundler.setup(:default, :development) +rescue Bundler::BundlerError => e + $stderr.puts e.message + $stderr.puts "Run `bundle install` to install missing gems" + exit e.status_code +end +require 'rake' + +require 'jeweler' +Jeweler::Tasks.new do |gem| + # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options + gem.name = "knife-azure" + gem.homepage = "http://github.com/northwestcommercial/knife-azure" + gem.license = "MIT" + gem.summary = %Q{TODO: one-line summary of your gem} + gem.description = %Q{TODO: longer description of your gem} + gem.email = "barryfromseattle@gmail.com" + gem.authors = ["Barry Davis"] + # dependencies defined in Gemfile +end +Jeweler::RubygemsDotOrgTasks.new + +require 'rspec/core' +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList['spec/unit/**/*_spec.rb'] +end + +RSpec::Core::RakeTask.new(:functional) do |spec| + spec.pattern = FileList['spec/functional/**/*_test.rb'] +end + +RSpec::Core::RakeTask.new(:integration) do |spec| + spec.pattern = FileList['spec/integration/**/*_test.rb'] +end + +RSpec::Core::RakeTask.new(:rcov) do |spec| + spec.pattern = 'spec/**/*_spec.rb' + spec.rcov = true +end + +task :default => :spec + +require 'rdoc/task' +Rake::RDocTask.new do |rdoc| + version = File.exist?('VERSION') ? File.read('VERSION') : "" + + rdoc.rdoc_dir = 'rdoc' + rdoc.title = "knife-azure #{version}" + rdoc.rdoc_files.include('README*') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/knife-azure.gemspec b/knife-azure.gemspec new file mode 100755 index 0000000..d79bc9e --- /dev/null +++ b/knife-azure.gemspec @@ -0,0 +1,22 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "knife-azure/version" + +Gem::Specification.new do |s| + s.name = "knife-azure" + s.version = Knife::Azure::VERSION + s.has_rdoc = true + s.authors = ["Barry Davis"] + s.email = ["barryd@jetstreamsoftware.com"] + s.homepage = "http://wiki.opscode.com/display/chef" + s.summary = "Azure Support for Chef's Knife Command" + s.description = s.summary + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.add_dependency "chef", "~> 0.10" + s.add_dependency "nokogiri" + s.require_paths = ["lib"] + +end diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000..6d94c58 Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/azure/connection.rb b/lib/azure/connection.rb new file mode 100755 index 0000000..ae5d17c --- /dev/null +++ b/lib/azure/connection.rb @@ -0,0 +1,75 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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. +# +require File.expand_path('../utility', __FILE__) +require File.expand_path('../rest', __FILE__) +require File.expand_path('../host', __FILE__) +require File.expand_path('../deploy', __FILE__) +require File.expand_path('../role', __FILE__) +require File.expand_path('../disk', __FILE__) +require File.expand_path('../image', __FILE__) + +class Azure + class Connection + include AzureAPI + attr_accessor :hosts, :rest, :images, :deploys, :roles, :disks + def initialize(params={}) + @rest = Rest.new(params) + @hosts = Hosts.new(self) + @images = Images.new(self) + @deploys = Deploys.new(self) + @roles = Roles.new(self) + @disks = Disks.new(self) + end + def query_azure(service_name, verb = 'get', body = '') + Chef::Log.info 'calling ' + verb + ' ' + service_name + Chef::Log.debug body unless body == '' + response = @rest.query_azure(service_name, verb, body) + if response.code.to_i == 200 + ret_val = Nokogiri::XML response.body + elsif response.code.to_i >= 201 && response.code.to_i <= 299 + ret_val = wait_for_completion() + else + if response.body + ret_val = Nokogiri::XML response.body + Chef::Log.warn ret_val.at_css('Error Code').content + ' : ' + ret_val.at_css('Error Message').content + else + Chef::Log.warn 'http error: ' + response.code + end + end + ret_val + end + def wait_for_completion() + status = 'InProgress' + Chef::Log.info 'Waiting while status returns InProgress' + while status == 'InProgress' + response = @rest.query_for_completion() + ret_val = Nokogiri::XML response.body + status = ret_val.at_css('Status').content + if status == 'InProgress' + print '.' + sleep(0.5) + elsif status == 'Succeeded' + Chef::Log.debug 'not InProgress : ' + ret_val.to_xml + else + Chef::Log.warn status + ret_val.at_css('Error Code').content + ' : ' + ret_val.at_css('Error Message').content + end + end + ret_val + end + end +end diff --git a/lib/azure/deploy.rb b/lib/azure/deploy.rb new file mode 100755 index 0000000..9e062a3 --- /dev/null +++ b/lib/azure/deploy.rb @@ -0,0 +1,114 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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 Deploys + def initialize(connection) + @connection=connection + end + def all + deploys = Array.new + hosts = @connection.hosts.all + hosts.each do |host| + deploy = Deploy.new(@connection) + deploy.retrieve(host.name) + unless deploy.name == nil + deploys << deploy + end + end + deploys + end + def find(hostedservicename) + deployName = nil + self.all.each do |deploy| + next unless deploy.hostedservicename == hostedservicename + deployName = deploy.name + end + deployName + end + def create(params) + unless @connection.hosts.exists(params[:hosted_service_name]) + @connection.hosts.create(params) + end + params['deploy_name'] = find(params[:hosted_service_name]) + if params['deploy_name'] != nil + role = Role.new(@connection) + roleXML = role.setup(params) + ret_val = role.create(params, roleXML) + else + params['deploy_name'] = params[:hosted_service_name] + deploy = Deploy.new(@connection) + deployXML = deploy.setup(params) + ret_val = deploy.create(params, deployXML) + end + if ret_val.css('Error Code').length > 0 + Chef::Log.fatal 'Unable to create role:' + ret_val.at_css('Error Code').content + ' : ' + ret_val.at_css('Error Message').content + exit 1 + end + @connection.roles.find(params[:role_name]) + end + def delete(rolename) + end + end + + class Deploy + include AzureUtility + attr_accessor :connection, :name, :status, :url, :roles, :hostedservicename + def initialize(connection) + @connection = connection + end + def retrieve(hostedservicename) + @hostedservicename = hostedservicename + deployXML = @connection.query_azure("hostedservices/#{hostedservicename}/deploymentslots/Production") + if deployXML.at_css('Deployment Name') != nil + @name = xml_content(deployXML, 'Deployment Name') + @status = xml_content(deployXML,'Deployment Status') + @url = xml_content(deployXML, 'Deployment Url') + @roles = Array.new + rolesXML = deployXML.css('Deployment RoleInstanceList RoleInstance') + rolesXML.each do |roleXML| + role = Role.new(@connection) + role.parse(roleXML, hostedservicename, @name) + @roles << role + end + end + end + def setup(params) + role = Role.new(@connection) + roleXML = role.setup(params) + #roleXML = Nokogiri::XML role.setup(params) + builder = Nokogiri::XML::Builder.new do |xml| + xml.Deployment( + 'xmlns'=>'http://schemas.microsoft.com/windowsazure', + 'xmlns:i'=>'http://www.w3.org/2001/XMLSchema-instance' + ) { + xml.Name params['deploy_name'] + xml.DeploymentSlot 'Production' + xml.Label Base64.encode64(params['deploy_name']).strip + xml.RoleList { xml.Role('i:type'=>'PersistentVMRole') } + } + end + builder.doc.at_css('Role') << roleXML.at_css('PersistentVMRole').children.to_s + builder.doc + end + def create(params, deployXML) + servicecall = "hostedservices/#{params[:hosted_service_name]}/deployments" + @connection.query_azure(servicecall, "post", deployXML.to_xml) + end + end +end diff --git a/lib/azure/disk.rb b/lib/azure/disk.rb new file mode 100755 index 0000000..491f628 --- /dev/null +++ b/lib/azure/disk.rb @@ -0,0 +1,62 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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 Disks + def initialize(connection) + @connection=connection + end + def all + disks = Array.new + response = @connection.query_azure('disks') + founddisks = response.css('Disk') + founddisks.each do |disk| + item = Disk.new(disk) + disks << item + end + disks + end + def find(name) + founddisk = nil + self.all.each do |disk| + next unless disk.name == name + founddisk = disk + end + founddisk + end + def exists(name) + find(name) != nil + end + def clear_unattached + self.all.each do |disk| + next unless disk.attached == false + @connection.query_azure('disks/' + disk.name, 'delete') + end + end + end +end + +class Azure + class Disk + attr_accessor :name, :attached + def initialize(disk) + @name = disk.at_css('Name').content + @attached = disk.at_css('AttachedTo') != nil + end + end +end diff --git a/lib/azure/host.rb b/lib/azure/host.rb new file mode 100755 index 0000000..57258d5 --- /dev/null +++ b/lib/azure/host.rb @@ -0,0 +1,90 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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 Hosts + def initialize(connection) + @connection=connection + end + def all + hosted_services = Array.new + responseXML = @connection.query_azure('hostedservices') + servicesXML = responseXML.css('HostedServices HostedService') + servicesXML.each do |serviceXML| + host = Host.new(@connection) + hosted_services << host.parse(serviceXML) + end + hosted_services + end + def exists(name) + hostExists = false + self.all.each do |host| + next unless host.name == name + hostExists = true + end + hostExists + end + def create(params) + host = Host.new(@connection) + host.create(params) + end + def delete(name) + if self.exists name + servicecall = "hostedservices/" + name + @connection.query_azure(servicecall, "delete") + end + end + end +end + +class Azure + class Host + include AzureUtility + attr_accessor :connection, :name, :url, :label + attr_accessor :dateCreated, :description, :location + attr_accessor :dateModified, :status + def initialize(connection) + @connection = connection + end + def parse(serviceXML) + @name = xml_content(serviceXML, 'ServiceName') + @url = xml_content(serviceXML, 'Url') + @label = xml_content(serviceXML, 'HostedServiceProperties Label') + @dateCreated = xml_content(serviceXML, 'HostedServiceProperties DateCreated') + @description = xml_content(serviceXML, 'HostedServiceProperties Description') + @location = xml_content(serviceXML, 'HostedServiceProperties Location') + @dateModified = xml_content(serviceXML, 'HostedServiceProperties DateLastModified') + @status = xml_content(serviceXML, 'HostedServiceProperties Status') + self + end + def create(params) + builder = Nokogiri::XML::Builder.new do |xml| + xml.CreateHostedService('xmlns'=>'http://schemas.microsoft.com/windowsazure') { + xml.ServiceName params[:hosted_service_name] + xml.Label Base64.encode64(params[:hosted_service_name]) + xml.Description params['hosted_service_description'] || 'Explicitly created hosted service' + xml.Location params['hosted_service_location'] || 'Windows Azure Preview' + } + end + @connection.query_azure("hostedservices", "post", builder.to_xml) + end + def details + response = @connection.query_azure('hostedservices/' + @name + '?embed-detail=true') + end + end +end diff --git a/lib/azure/image.rb b/lib/azure/image.rb new file mode 100755 index 0000000..d853c4a --- /dev/null +++ b/lib/azure/image.rb @@ -0,0 +1,58 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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 Images + def initialize(connection) + @connection=connection + end + def all + images = Array.new + response = @connection.query_azure('images') + osimages = response.css('OSImage') + osimages.each do |image| + item = Image.new(image) + images << item + end + images + end + def exists(name) + imageExists = false + self.all.each do |host| + next unless host.name == name + imageExists = true + end + imageExists + end + end +end + +class Azure + class Image + attr_accessor :category, :label + attr_accessor :name, :os, :eula, :description + def initialize(image) + @category = image.at_css('Category').content + @label = image.at_css('Label').content + @name = image.at_css('Name').content + @os = image.at_css('OS').content + @eula = image.at_css('Eula').content + @description = image.at_css('Description').content + end + end +end diff --git a/lib/azure/rest.rb b/lib/azure/rest.rb new file mode 100755 index 0000000..7c15395 --- /dev/null +++ b/lib/azure/rest.rb @@ -0,0 +1,97 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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. +# +require "net/https" +require "uri" +require "nokogiri" + +module AzureAPI + + class Rest + def initialize(params) + @subscription_id = params[:azure_subscription_id] + @pem_file = File.read find_pem(params[:azure_pem_file]) + @host_name = params[:azure_host_name] + end + def find_pem(name) + config_dir = Chef::Knife.chef_config_dir + if File.exist? name + pem_file = name + elsif config_dir && File.exist?(File.join(config_dir, name)) + pem_file = File.join(config_dir, name) + elsif File.exist?(File.join(ENV['HOME'], '.chef', name)) + pem_file = File.join(ENV['HOME'], '.chef', name) + else + raise 'Unable to find certificate pem file - ' + name + end + pem_file + end + def query_azure(service_name, verb = 'get', body = '') + request_url = "https://#{@host_name}/#{@subscription_id}/services/#{service_name}" + print '.' + uri = URI.parse(request_url) + http = http_setup(uri) + request = request_setup(uri, verb, body) + response = http.request(request) + @last_request_id = response['x-ms-request-id'] + response + end + def query_for_completion() + request_url = "https://#{@host_name}/#{@subscription_id}/operations/#{@last_request_id}" + uri = URI.parse(request_url) + http = http_setup(uri) + request = request_setup(uri, 'get', '') + response = http.request(request) + end + def http_setup(uri) + http = Net::HTTP.new(uri.host, uri.port) + store = OpenSSL::X509::Store.new + store.add_cert(OpenSSL::X509::Certificate.new(File.read(find_pem("cacert.pem")))) + http.cert_store = store + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.use_ssl = true + http.cert = OpenSSL::X509::Certificate.new(@pem_file) + http.key = OpenSSL::PKey::RSA.new(@pem_file) + http + end + def request_setup(uri, verb, body) + if verb == 'get' + request = Net::HTTP::Get.new(uri.request_uri) + elsif verb == 'post' + request = Net::HTTP::Post.new(uri.request_uri) + elsif verb == 'delete' + request = Net::HTTP::Delete.new(uri.request_uri) + end + request["x-ms-version"] = "2012-03-01" + request["content-type"] = "application/xml" + request["accept"] = "application/xml" + request["accept-charset"] = "utf-8" + request.body = body + request + end + def showResponse(response) + puts "=== response body ===" + puts response.body + puts "=== response.code ===" + puts response.code + puts "=== response.inspect ===" + puts response.inspect + puts "=== all of the headers ===" + puts response.each_header { |h, j| puts h.inspect + ' : ' + j.inspect} + end + end +end diff --git a/lib/azure/role.rb b/lib/azure/role.rb new file mode 100755 index 0000000..d23b143 --- /dev/null +++ b/lib/azure/role.rb @@ -0,0 +1,182 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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 Roles + attr_accessor :connection, :roles + def initialize(connection) + @connection = connection + @roles = nil + end + def all + @roles = Array.new + @connection.deploys.all.each do |deploy| + deploy.roles.each do |role| + @roles << role + end + end + @roles + end + def find(name) + if @roles == nil + all + end + @roles.each do |role| + if(role.name == name) + return role + end + end + nil + end + def alone_on_host(name) + found_role = find(name) + @roles.each do |role| + if (role.name != found_role.name && + role.deployname == found_role.deployname && + role.hostedservicename == found_role.hostedservicename) + return false; + end + end + true + end + def exists(name) + find(name) != nil + end + def delete(name) + role = find(name) + if role != nil + if alone_on_host(name) + servicecall = "hostedservices/#{role.hostedservicename}/deployments" + + "/#{role.deployname}" + else + servicecall = "hostedservices/#{role.hostedservicename}/deployments" + + "/#{role.deployname}/roles/#{role.name}" + end + @connection.query_azure(servicecall, "delete") + end + #@connection.disks.clear_unattached + end + end + class Role + include AzureUtility + attr_accessor :connection, :name, :status, :size, :ipaddress + attr_accessor :sshport, :sshipaddress, :hostedservicename, :deployname + attr_accessor :hostname, :tcpports, :udpports + def initialize(connection) + @connection = connection + end + def parse(roleXML, hostedservicename, deployname) + @name = xml_content(roleXML, 'RoleName') + @status = xml_content(roleXML, 'InstanceStatus') + @size = xml_content(roleXML, 'InstanceSize') + @ipaddress = xml_content(roleXML, 'IpAddress') + @hostname = xml_content(roleXML, 'HostName') + @hostedservicename = hostedservicename + @deployname = deployname + @tcpports = Array.new + @udpports = Array.new + endpoints = roleXML.css('InstanceEndpoint') + endpoints.each do |endpoint| + if xml_content(endpoint, 'Name').downcase == 'ssh' + @sshport = xml_content(endpoint, 'PublicPort') + @sshipaddress = xml_content(endpoint, 'Vip') + else + hash = Hash.new + hash['Name'] = xml_content(endpoint, 'Name') + hash['Vip'] = xml_content(endpoint, 'Vip') + hash['PublicPort'] = xml_content(endpoint, 'PublicPort') + hash['LocalPort'] = xml_content(endpoint, 'LocalPort') + if xml_content(endpoint, 'Protocol') == 'tcp' + @tcpports << hash + else # == 'udp' + @udpports << hash + end + end + end + end + def setup(params) + builder = Nokogiri::XML::Builder.new do |xml| + xml.PersistentVMRole( + 'xmlns'=>'http://schemas.microsoft.com/windowsazure', + 'xmlns:i'=>'http://www.w3.org/2001/XMLSchema-instance' + ) { + xml.RoleName {xml.text params[:role_name]} + xml.OsVersion('i:nil' => 'true') + xml.RoleType 'PersistentVMRole' + xml.ConfigurationSets { + 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.ConfigurationSet('i:type' => 'NetworkConfigurationSet') { + xml.ConfigurationSetType 'NetworkConfiguration' + xml.InputEndpoints { + xml.InputEndpoint { + xml.LocalPort '22' + xml.Name 'SSH' + xml.Protocol 'TCP' + } + if params[:tcp_endpoints] + params[:tcp_endpoints].split(',').each do |endpoint| + ports = endpoint.split(':') + xml.InputEndpoint { + xml.LocalPort ports[0] + xml.Name 'tcpport_' + ports[0] + '_' + params[:host_name] + if ports.length > 1 + xml.Port ports[1] + end + xml.Protocol 'TCP' + } + end + end + if params[:udp_endpoints] + params[:udp_endpoints].split(',').each do |endpoint| + ports = endpoint.split(':') + xml.InputEndpoint { + xml.LocalPort ports[0] + xml.Name 'udpport_' + ports[0] + '_' + params[:host_name] + if ports.length > 1 + xml.Port ports[1] + end + xml.Protocol 'UDP' + } + end + end + } + } + } + xml.Label Base64.encode64(params[:role_name]).strip + xml.OSVirtualHardDisk { + xml.MediaLink 'http://' + params[:media_location_prefix] + 'imagestore.blob.core.azure-preview.com/os-disks/' + (params[:os_disk_name] || Time.now.strftime('disk_%Y_%m_%d_%H_%M')) + xml.SourceImageName params[:source_image] + } + xml.RoleSize params[:role_size] + } + end + builder.doc + end + def create(params, roleXML) + servicecall = "hostedservices/#{params[:hosted_service_name]}/deployments" + + "/#{params['deploy_name']}/roles" + @connection.query_azure(servicecall, "post", roleXML.to_xml) + end + end +end diff --git a/lib/azure/utility.rb b/lib/azure/utility.rb new file mode 100755 index 0000000..ae62fbe --- /dev/null +++ b/lib/azure/utility.rb @@ -0,0 +1,29 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.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. +# + +module AzureUtility + def xml_content(xml, key, default='') + content = default + node = xml.at_css(key) + if node + content = node.content + end + content + end +end + diff --git a/lib/chef/.DS_Store b/lib/chef/.DS_Store new file mode 100644 index 0000000..fdceebf Binary files /dev/null and b/lib/chef/.DS_Store differ diff --git a/lib/chef/knife/azure_base.rb b/lib/chef/knife/azure_base.rb new file mode 100755 index 0000000..808d99d --- /dev/null +++ b/lib/chef/knife/azure_base.rb @@ -0,0 +1,102 @@ + +# Author:: Barry Davis (barryd@jetstreamsoftware.com) +# Author:: Seth Chisamore () +# Copyright:: Copyright (c) 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. +# + +require 'chef/knife' +require File.expand_path('../../../azure/connection', __FILE__) + +class Chef + class Knife + module AzureBase + + # :nodoc: + # Would prefer to do this in a rational way, but can't be done b/c of + # Mixlib::CLI's design :( + def self.included(includer) + includer.class_eval do + + deps do + require 'readline' + require 'chef/json_compat' + end + + option :azure_subscription_id, + :short => "-S ID", + :long => "--azure-subscription-id ID", + :description => "Your Azure subscription ID", + :proc => Proc.new { |key| Chef::Config[:knife][:azure_subscription_id] = key } + + option :azure_pem_file, + :short => "-p FILENAME", + :long => "--azure-pem-filename FILENAME", + :description => "Your Azure PEM file name", + :proc => Proc.new { |key| Chef::Config[:knife][:azure_pem_file] = key } + + option :azure_host_name, + :short => "-H HOSTNAME", + :long => "--azure_host_name HOSTNAME", + :description => "Your Azure host name", + :proc => Proc.new { |key| Chef::Config[:knife][:azure_host_name] = key } + + end + end + + def connection + @connection ||= begin + connection = Azure::Connection.new( + :azure_subscription_id => + config[:azure_subscription_id] || Chef::Config[:knife][:azure_subscription_id], + :azure_pem_file => + config[:azure_pem_file] || Chef::Config[:knife][:azure_pem_file], + :azure_host_name => + config[:azure_host_name] || Chef::Config[:knife][:azure_host_name] + ) + end + end + + def locate_config_value(key) + key = key.to_sym + config[key] || Chef::Config[:knife][key] + end + + def msg_pair(label, value, color=:cyan) + if value && !value.to_s.empty? + puts "#{ui.color(label, color)}: #{value}" + end + end + + def validate!(keys=[:azure_subscription_id, :azure_pem_file, :azure_host_name]) + errors = [] + + keys.each do |k| + pretty_key = k.to_s.gsub(/_/, ' ').gsub(/\w+/){ |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize } + if Chef::Config[:knife][k].nil? + errors << "You did not provide a valid '#{pretty_key}' value." + end + end + + if errors.each{|e| ui.error(e)}.any? + exit 1 + end + end + + end + end +end + + diff --git a/lib/chef/knife/azure_image_list.rb b/lib/chef/knife/azure_image_list.rb new file mode 100755 index 0000000..131e86e --- /dev/null +++ b/lib/chef/knife/azure_image_list.rb @@ -0,0 +1,58 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.com) +# Author:: Seth Chisamore () +# Author:: Adam Jacob () +# 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. +# + +require File.expand_path('../azure_base', __FILE__) + +class Chef + class Knife + class AzureImageList < Knife + + include Knife::AzureBase + + banner "knife azure image list (options)" + + def run + $stdout.sync = true + + validate! + + image_list = [ + ui.color('Name', :bold), + #ui.color('Category', :bold), + #ui.color('Label', :bold), + #ui.color('OS', :bold), + #ui.color('Eula', :bold), + ] + items = connection.images.all + items.each do |image| + if image.os == 'Linux' + image_list << image.name.to_s + #image_list << image.category.to_s + #image_list << image.label.to_s + #image_list << image.os.to_s + #image_list << image.eula.to_s + end + end + puts '' + puts ui.list(image_list, :columns_across, 1) + end + end + end +end diff --git a/lib/chef/knife/azure_server_create.rb b/lib/chef/knife/azure_server_create.rb new file mode 100755 index 0000000..9cf9326 --- /dev/null +++ b/lib/chef/knife/azure_server_create.rb @@ -0,0 +1,282 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.com) +# Author:: Adam Jacob () +# Author:: Seth Chisamore () +# 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. +# + +require File.expand_path('../azure_base', __FILE__) + +class Chef + class Knife + class AzureServerCreate < Knife + + include Knife::AzureBase + + deps do + require 'readline' + require 'chef/json_compat' + require 'chef/knife/bootstrap' + Chef::Knife::Bootstrap.load_deps + end + + banner "knife azure server create (options)" + + attr_accessor :initial_sleep_delay + + option :chef_node_name, + :short => "-N NAME", + :long => "--node-name NAME", + :description => "The Chef node name for your new node" + + option :ssh_user, + :short => "-x USERNAME", + :long => "--ssh-user USERNAME", + :description => "The ssh username" + + option :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" + + option :prerelease, + :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 } + + option :distro, + :short => "-d DISTRO", + :long => "--distro DISTRO", + :description => "Bootstrap a distro using a template", + :proc => Proc.new { |d| Chef::Config[:knife][:distro] = d } + + 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 + + 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 => [] + + option :no_host_key_verify, + :long => "--no-host-key-verify", + :description => "Disable host key verification", + :boolean => true, + :default => false + + option :hosted_service_name, + :short => "-s NAME", + :long => "--hosted-service-name NAME", + :description => "specifies the name for the hosted service" + + option :role_name, + :short => "-R name", + :long => "--role-name NAME", + :description => "specifies the name for the virtual machine" + + option :host_name, + :short => "-H NAME", + :long => "--host-name NAME", + :description => "specifies the host name for the virtual machine" + + option :media_location_prefix, + :short => "-m PREFIX", + :long => "--media-location-prefix PREFIX", + :description => "user account name (used for constructing os disk media link)" + + option :os_disk_name, + :short => "-o DISKNAME", + :long => "--os-disk-name DISKNAME", + :description => "unique name for specifying os disk (optional)" + + option :source_image, + :short => "-I IMAGE", + :long => "--source-image IMAGE", + :description => "disk image name to use to create virtual machine" + + option :role_size, + :short => "-z SIZE", + :long => "--role-size SIZE", + :description => "size of virtual machine (ExtraSmall, Small, Medium, Large, ExtraLarge)" + + option :tcp_endpoints, + :short => "-t PORT_LIST", + :long => "--tcp-endpoints PORT_LIST", + :description => "Comma separated list of TCP local and public ports to open i.e. '80:80,433:5000'" + + option :udp_endpoints, + :short => "-u PORT_LIST", + :long => "--udp-endpoints PORT_LIST", + :description => "Comma separated list of UDP local and public ports to open i.e. '80:80,433:5000'" + + + def tcp_test_ssh(fqdn, sshport) + tcp_socket = TCPSocket.new(fqdn, sshport) + readable = IO.select([tcp_socket], nil, nil, 5) + if readable + Chef::Log.debug("sshd accepting connections on #{fqdn}, banner is #{tcp_socket.gets}") + yield + true + else + false + end + rescue SocketError + sleep 2 + false + rescue Errno::ETIMEDOUT + false + rescue Errno::EPERM + false + rescue Errno::ECONNREFUSED + sleep 2 + false + # This happens on EC2 quite often + rescue Errno::EHOSTUNREACH + sleep 2 + false + ensure + tcp_socket && tcp_socket.close + end + + def parameter_test + details = Array.new + details << ui.color('name', :bold, :blue) + details << ui.color('Chef::Config', :bold, :blue) + details << ui.color('config', :bold, :blue) + details << ui.color('winner is', :bold, :blue) + [ + :azure_subscription_id, + :azure_pem_file, + :azure_host_name, + :hosted_service_name, + :role_name, + :host_name, + :ssh_user, + :ssh_password, + :media_location_prefix, + :source_image, + :role_size + ].each do |key| + key = key.to_sym + details << key.to_s + details << Chef::Config[:knife][key].to_s + details << config[key].to_s + details << locate_config_value(key) + end + puts ui.list(details, :columns_across, 4) + end + def run + $stdout.sync = true + + Chef::Log.info("validating...") + validate! + + Chef::Log.info("creating...") + server = connection.deploys.create(create_server_def) + + puts("\n") + + unless server && server.sshipaddress && server.sshport + Chef::Log.fatal("server not created") + exit 1 + end + + fqdn = server.sshipaddress + port = server.sshport + + print "\n#{ui.color("Waiting for sshd on #{fqdn}:#{port}", :magenta)}" + + print(".") until tcp_test_ssh(fqdn,port) { + sleep @initial_sleep_delay ||= 10 + puts("done") + } + + sleep 15 + + bootstrap_for_node(server,fqdn,port).run + + puts "\n" + end + + def bootstrap_for_node(server,fqdn,port) + bootstrap = Chef::Knife::Bootstrap.new + bootstrap.name_args = [fqdn] + bootstrap.config[:run_list] = config[:run_list] + bootstrap.config[:ssh_user] = locate_config_value(:ssh_user) + bootstrap.config[:ssh_password] = locate_config_value(:ssh_password) + bootstrap.config[:ssh_port] = port + bootstrap.config[:identity_file] = locate_config_value(:identity_file) + bootstrap.config[:chef_node_name] = locate_config_value(:chef_node_name) || server.name + bootstrap.config[:prerelease] = locate_config_value(:prerelease) + bootstrap.config[:bootstrap_version] = locate_config_value(:bootstrap_version) + bootstrap.config[:distro] = locate_config_value(:distro) + bootstrap.config[:use_sudo] = true unless locate_config_value(:ssh_user) == 'root' + bootstrap.config[:template_file] = config[:template_file] + bootstrap.config[:environment] = locate_config_value(:environment) + # may be needed for vpc_mode + bootstrap.config[:no_host_key_verify] = config[:no_host_key_verify] + bootstrap + end + + def validate! + super([ + :azure_subscription_id, + :azure_pem_file, + :azure_host_name, + :hosted_service_name, + :role_name, + :host_name, + :ssh_user, + :ssh_password, + :media_location_prefix, + :source_image, + :role_size + ]) + end + + def create_server_def + server_def = { + :hosted_service_name => locate_config_value(:hosted_service_name), + :role_name => locate_config_value(:role_name), + :host_name => locate_config_value(:host_name), + :ssh_user => locate_config_value(:ssh_user), + :ssh_password => locate_config_value(:ssh_password), + :media_location_prefix => locate_config_value(:media_location_prefix), + :os_disk_name => locate_config_value(:os_disk_name), + :source_image => locate_config_value(:source_image), + :role_size => locate_config_value(:role_size), + :tcp_endpoints => locate_config_value(:tcp_endpoints), + :udp_endpoints => locate_config_value(:udp_endpoints) + } + server_def + end + end + end +end diff --git a/lib/chef/knife/azure_server_delete.rb b/lib/chef/knife/azure_server_delete.rb new file mode 100755 index 0000000..0c85635 --- /dev/null +++ b/lib/chef/knife/azure_server_delete.rb @@ -0,0 +1,103 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.com) +# Author:: Adam Jacob () +# Author:: Seth Chisamore () +# Copyright:: Copyright (c) 2009-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. +# + +require File.expand_path('../azure_base', __FILE__) + +# These two are needed for the '--purge' deletion case +require 'chef/node' +require 'chef/api_client' + +class Chef + class Knife + class AzureServerDelete < Knife + + include Knife::AzureBase + + banner "knife azure server delete SERVER [SERVER] (options)" + + option :purge, + :short => "-P", + :long => "--purge", + :boolean => true, + :default => false, + :description => "Destroy corresponding node and client on the Chef Server, in addition to destroying the EC2 node itself. Assumes node and client have the same name as the server (if not, add the '--node-name' option)." + + option :chef_node_name, + :short => "-N NAME", + :long => "--node-name NAME", + :description => "The name of the node and client to delete, if it differs from the server name. Only has meaning when used with the '--purge' option." + + # Extracted from Chef::Knife.delete_object, because it has a + # confirmation step built in... By specifying the '--purge' + # flag (and also explicitly confirming the server destruction!) + # the user is already making their intent known. It is not + # necessary to make them confirm two more times. + def destroy_item(klass, name, type_name) + begin + object = klass.load(name) + object.destroy + ui.warn("Deleted #{type_name} #{name}") + rescue Net::HTTPServerException + ui.warn("Could not find a #{type_name} named #{name} to delete!") + end + end + + def run + + validate! + + @name_args.each do |name| + + begin + server = connection.roles.find(name) + + puts "\n" + msg_pair('Service', server.hostedservicename) + msg_pair('Deployment', server.deployname) + msg_pair('Role', server.name) + msg_pair('Size', server.size) + msg_pair('SSH Ip Address', server.sshipaddress) + msg_pair('SSH Port', server.sshport) + + puts "\n" + confirm("Do you really want to delete this server") + + connection.roles.delete(name) + + puts "\n" + ui.warn("Deleted server #{server.name}") + + if config[:purge] + thing_to_delete = config[:chef_node_name] || name + destroy_item(Chef::Node, thing_to_delete, "node") + destroy_item(Chef::ApiClient, thing_to_delete, "client") + else + ui.warn("Corresponding node and client for the #{name} server were not deleted and remain registered with the Chef Server") + end + + rescue NoMethodError + ui.error("Could not locate server '#{name}'. Please verify it was provisioned.") + end + end + end + + end + end +end diff --git a/lib/chef/knife/azure_server_describe.rb b/lib/chef/knife/azure_server_describe.rb new file mode 100755 index 0000000..a06822c --- /dev/null +++ b/lib/chef/knife/azure_server_describe.rb @@ -0,0 +1,85 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.com) +# Author:: Seth Chisamore () +# Author:: Adam Jacob () +# 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. +# + +require File.expand_path('../azure_base', __FILE__) + +class Chef + class Knife + class AzureServerDescribe < Knife + + include Knife::AzureBase + + banner "knife azure server describe ROLE [ROLE]" + + def run + $stdout.sync = true + + validate! + + @name_args.each do |name| + role = connection.roles.find name + puts '' + if (role) + details = Array.new + details << ui.color('Role name', :bold, :blue) + details << role.name + details << ui.color('Status', :bold, :blue) + details << role.status + details << ui.color('Size', :bold, :blue) + details << role.size + details << ui.color('Hosted service name', :bold, :blue) + details << role.hostedservicename + details << ui.color('Deployment name', :bold, :blue) + details << role.deployname + details << ui.color('Host name', :bold, :blue) + details << role.hostname + details << ui.color('SSH', :bold, :blue) + details << role.sshipaddress + ':' + role.sshport + puts ui.list(details, :columns_across, 2) + if role.tcpports.length > 0 || role.udpports.length > 0 + details.clear + details << ui.color('Ports open', :bold, :blue) + details << ui.color('Local port', :bold, :blue) + details << ui.color('IP', :bold, :blue) + details << ui.color('Public port', :bold, :blue) + if role.tcpports.length > 0 + role.tcpports.each do |port| + details << 'tcp' + details << port['LocalPort'] + details << port['Vip'] + details << port['PublicPort'] + end + end + if role.udpports.length > 0 + role.udpports.each do |port| + details << 'udp' + details << port['LocalPort'] + details << port['Vip'] + details << port['PublicPort'] + end + end + puts ui.list(details, :columns_across, 4) + end + end + end + end + end + end +end diff --git a/lib/chef/knife/azure_server_list.rb b/lib/chef/knife/azure_server_list.rb new file mode 100755 index 0000000..8e28057 --- /dev/null +++ b/lib/chef/knife/azure_server_list.rb @@ -0,0 +1,70 @@ +# +# Author:: Barry Davis (barryd@jetstreamsoftware.com) +# Author:: Seth Chisamore () +# Author:: Adam Jacob () +# 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. +# + +require File.expand_path('../azure_base', __FILE__) + +class Chef + class Knife + class AzureServerList < Knife + + include Knife::AzureBase + + banner "knife azure server list (options)" + + def run + $stdout.sync = true + + validate! + + server_list = [ + ui.color('Status', :bold), + ui.color('Service', :bold), + ui.color('Deployment', :bold), + ui.color('Role', :bold), + ui.color('Host', :bold), + ui.color('SSH IP', :bold), + ui.color('SSH Port', :bold) + ] + items = connection.roles.all + items.each do |server| + server_list << begin + state = server.status.to_s.downcase + case state + when 'shutting-down','terminated','stopping','stopped' + ui.color(state, :red) + when 'pending' + ui.color(state, :yellow) + else + ui.color('ready', :green) + end + end + server_list << server.hostedservicename.to_s + server_list << server.deployname.to_s + server_list << server.name.to_s + server_list << server.hostname.to_s + server_list << server.sshipaddress.to_s + server_list << server.sshport.to_s + end + puts '' + puts ui.list(server_list, :columns_across, 7) + end + end + end +end diff --git a/lib/knife-azure/version.rb b/lib/knife-azure/version.rb new file mode 100755 index 0000000..3ccf529 --- /dev/null +++ b/lib/knife-azure/version.rb @@ -0,0 +1,7 @@ +module Knife + module Azure + VERSION = "0.5.11" + MAJOR, MINOR, TINY = VERSION.split('.') + end +end + diff --git a/readme.rdoc b/readme.rdoc new file mode 100755 index 0000000..aec4083 --- /dev/null +++ b/readme.rdoc @@ -0,0 +1,199 @@ +=Knife Azure + +Description: This plugin supports listing, creating, and deleting Azure instances bootstrapped with chef client. + +==Installation: +Be sure you are running the latest version Chef. Versions earlier than 0.10.0 don’t support plugins: + +gem install chef +This plugin is distributed as a Ruby Gem. To install it, run: + +gem install knife-azure +Depending on your system’s configuration, you may need to run this command with root privileges. + + +==Configuration: +Most configuration options can be specified either in your knife.rb file or as command line parameters. + +Options common and necessary for all subcommands: +option :azure_subscription_id, + :short => "-S ID", + :long => "--azure-subscription-id ID", + :description => "Your Azure subscription ID", + +option :azure_pem_file, + :short => "-p FILENAME", + :long => "--azure-pem-filename FILENAME", + :description => "Your Azure PEM file name", + +option :azure_host_name, + :short => "-H HOSTNAME", + :long => "--azure_host_name HOSTNAME", + :description => "Your Azure host name", + +Options used with the Create subcommand: +option :chef_node_name, + :short => "-N NAME", + :long => "--node-name NAME", + :description => "The Chef node name for your new node" + +option :ssh_user, + :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" + +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" + +option :bootstrap_version, + :long => "--bootstrap-version VERSION", + :description => "The version of Chef to install", + +option :distro, + :short => "-d DISTRO", + :long => "--distro DISTRO", + :description => "Bootstrap a distro using a template", + :default => "ubuntu10.04-gems" + +option :template_file, + :long => "--template-file TEMPLATE", + :description => "Full path to location of template to use", + :default => false + +option :run_list, + :short => "-r RUN_LIST", + :long => "--run-list RUN_LIST", + :description => "Comma separated list of roles/recipes to apply", + :default => [] + +option :no_host_key_verify, + :long => "--no-host-key-verify", + :description => "Disable host key verification", + :boolean => true, + :default => false + +option :hosted_service_name, + :short => "-s NAME", + :long => "--hosted-service-name NAME", + :description => "specifies the name for the hosted service" + +option :role_name, + :short => "-R name", + :long => "-- role-name NAME", + :description => "specifies the name for the virtual machine" + +option :host_name, + :short => "-H NAME", + :long => "--host-name NAME", + :description => "specifies the host name for the virtual machine" + +option :media_location_prefix, + :short => "-m PREFIX", + :long => "--media-location-prefix PREFIX", + :description => "user account name (used for constructing os disk media link)" + +option :os_disk_name, + :short => "-o DISKNAME", + :long => "--os-disk-name DISKNAME", + :description => "unique name for specifying os disk (optional)" + +option :source_image, + :short => "-I IMAGE", + :long => "--source-image IMAGE", + :description => "disk image name to use to create virtual machine" + +option :role_size, + :short => "-z SIZE", + :long => "--role-size SIZE", + :description => "size of virtual machine (ExtraSmall, Small, Medium, Large, ExtraLarge)" + +option :tcp_endpoints, + :short => "-t PORT_LIST", + :long => "--tcp-endpoints PORT_LIST", + :description => "Comma separated list of TCP local and public ports to open i.e. '80:80,433:5000'" + +option :udp_endpoints, + :short => "-u PORT_LIST", + :long => "--udp-endpoints PORT_LIST", + :description => "Comma separated list of UDP local and public ports to open i.e. '80:80,433:5000'" + + +====Here are some lines with example values in a knife.rb file: +knife[:azure_subscription_id] = "YOUR-GUID" + +knife[:azure_pem_file] = "YOUR-CERT.pem" + +knife[:azure_host_name] = "azure-api-endpoint" + +knife[:hosted_service_name]='service001' + +knife[:role_name]='role105' + +knife[:host_name]='host105' + +knife[:ssh_user]='yoursshuser' + +knife[:ssh_password]='yoursshpw' + +knife[:media_location_prefix]='auxpreview104' + +knife[:os_disk_name]='disk107' + +knife[:distro]='centos5-gems' + +knife[:tcp_endpoints]='66' + +knife[:udp_endpoints]='77,88,99' + +# To use the CentOS image, the following lines are necessary + +# note that the role_size must be Medium or larger + +knife[:source_image]='OpenLogic__OpenLogic-CentOS-62-20120509-en-us-30GB.vhd' + +knife[:role_size]='Medium' + +# Alternatively, at the present time you could use a SUSE image + +# note that you can use Small or ExtraSmall for the role_size + +knife[:source_image]='SUSE__OpenSUSE64121-03192012-en-us-15GB.vhd' + +knife[:role_size]='Small' + +==Subcommands +This plugin provides the following Knife subcommands. Specific command options can be found by invoking the subcommand with a --help flag + +===knife azure server create +Provisions a new server in Azure and then perform a Chef bootstrap (using the SSH protocol). The goal of the bootstrap is to get Chef installed on the target system so it can run Chef Client with a Chef Server. The main assumption is a baseline OS installation exists (provided by the provisioning). It is primarily intended for Chef Client systems that talk to a Chef server. By default the server is bootstrapped using the ubuntu10.04-gems template. This can be overridden using the -d or --template-file command options. + +===knife azure server delete [role_name_to_delete] +Deletes an existing server(role) in the currently configured AWS account. PLEASE NOTE - By default, this does not delete the associated node and client objects from the Chef server. To do so, add the --purge flag. + +===knife azure server list +Outputs a list of all servers in the currently configured AWS account. PLEASE NOTE - this shows all instances associated with the account, some of which may not be currently managed by the Chef server. + +===knife azure server describe [role_name_to_describe] +Outputs detail about a specific role, including all the ports that it has open + +===knife azure image list +Outputs a list of all linux images that are available to use for provisioning. You should choose one of these to use for the :source_image parameter to the server create command. + +== Understanding Azure +Azure implements the following hierarchy - subscription=>hosted service=>deployment=>role (and the guest operating system has a hostname as well, which uses the role as its container) + +These are generally a one to many relationship from top to bottom, however there are two anamolies relating to the deployment +1) a hosted service can have more than one deployment, but that seems to be an artifact of the PAAS origins of Azure. PAAS allows there to be one staging and one production deployment per hosted service. It is my understanding (and how the code works) that there should be only one deployment per hosted service. Some initial internal code I examined used the technique of looking at the "production" deployment slot to iterate for existing roles. If a create request occurs and a deployment does not exist, it is created and given the same name as the hosted service and the deployment slot is marked as "production". +2) Azure enforces that a deployment must include the initial role when it is created. It also will not allow you to delete a role if it is the last remaining role in a deployment; in that case you are required to delete the deployment. diff --git a/spec/functional/deploys_test.rb b/spec/functional/deploys_test.rb new file mode 100755 index 0000000..8b5875d --- /dev/null +++ b/spec/functional/deploys_test.rb @@ -0,0 +1,43 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe "deploys" do + before(:all) do + params = { :azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", + :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + @connection = Azure::Connection.new(params) + @deploys = @connection.deploys.all + end + + specify {@deploys.length.should be > 0} + it 'each deployment should have values' do + @deploys.each do |deploy| + deploy.name.should_not be_nil + deploy.status.should_not be_nil + deploy.url.should_not be_nil + deploy.roles.length.should be > 0 + end + end + it 'each role should have values' do + @deploys.each do |deploy| + Chef::Log.info '=============================' + Chef::Log.info 'hosted service: ' + deploy.hostedservicename + ' deployment: ' + deploy.name + deploy.roles.each do |role| + role.name.should_not be_nil + role.status.should_not be_nil + role.size.should_not be_nil + role.ipaddress.should_not be_nil + role.sshport.should_not be_nil + role.sshipaddress.should_not be_nil + Chef::Log.info '=============================' + Chef::Log.info 'role: ' + role.name + Chef::Log.info 'status: ' + role.status + Chef::Log.info 'size: ' + role.size + Chef::Log.info 'ip address: ' + role.ipaddress + Chef::Log.info 'ssh port: ' + role.sshport + Chef::Log.info 'ssh ip address: ' + role.sshipaddress + end + end + end +end diff --git a/spec/functional/host_test.rb b/spec/functional/host_test.rb new file mode 100755 index 0000000..dc449a4 --- /dev/null +++ b/spec/functional/host_test.rb @@ -0,0 +1,26 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe "Connection" do + + before(:all) do + params = { :azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", + :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + @connection = Azure::Connection.new(params) + @items = @connection.hosts.all + end + + specify {@items.length.should be > 0} + specify {@connection.hosts.exists("thisServiceShouldNotBeThere").should == false} + specify{@connection.hosts.exists("service002").should == true} + it "looking for a specific host" do + foundNamedHost = false + @items.each do |host| + next unless host.name == "service002" + foundNamedHost = true + end + foundNamedHost.should == true + end +end + diff --git a/spec/functional/images_list_test.rb b/spec/functional/images_list_test.rb new file mode 100755 index 0000000..fd62527 --- /dev/null +++ b/spec/functional/images_list_test.rb @@ -0,0 +1,48 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe "Connection" do + + before(:all) do + params = { :azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", + :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + @connection = Azure::Connection.new(params) + @items = @connection.images.all + end + + it "should be contain images" do + @items.length.should be > 1 + end + it "each image should have all fields valid" do + @items.each do |image| + image.category.should_not be_nil + image.label.should_not be_nil + image.name.should_not be_nil + image.os.should_not be_nil + image.eula.should_not be_nil + image.description.should_not be_nil + end + end + + + # it "should get services" do + # @demo.DemoGet + # end + # it "bad subscription should fail with ResourceNotFound" do + # @demo.subscription = "ae2ff9b3-12b2-45cf-b58e-468bc7e29110xxxxx" + # + # expect{@demo.DemoGet}.to raise_error(RuntimeError, /ResourceNotFound/) + # end + # it "bad pem_path should fail with CertificateError" do + # @demo.pem_file = "" + # + # expect{@demo.DemoGet}.to raise_error(OpenSSL::X509::CertificateError) + # end + # it "bad service_name should fail with " do + # @demo.service_name = "" + # + # expect{@demo.DemoGet}.to raise_error(RuntimeError, /ResourceNotFound/) + # end +end + diff --git a/spec/functional/role_test.rb b/spec/functional/role_test.rb new file mode 100755 index 0000000..7976939 --- /dev/null +++ b/spec/functional/role_test.rb @@ -0,0 +1,20 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe "roles" do + before(:all) do + params = { :azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", + :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + @connection = Azure::Connection.new(params) + @roles = @connection.roles.all + end + + specify {@connection.roles.exists('notexist').should == false} + specify {@connection.roles.exists('role126').should == true} + it 'run through roles' do + @connection.roles.roles.each do |role| + role.name.should_not be_nil + end + end +end diff --git a/spec/integration/role_lifecycle_test.rb b/spec/integration/role_lifecycle_test.rb new file mode 100755 index 0000000..3b58743 --- /dev/null +++ b/spec/integration/role_lifecycle_test.rb @@ -0,0 +1,62 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +describe "role lifecycle" do + Chef::Log.init() + Chef::Log.level=:info + before(:all) do + connection_params = { :azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", + :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + @connection = Azure::Connection.new(connection_params) + arbitrary = rand(1000) + 1 + @params = { + :hosted_service_name=>'service002', + :role_name=>'role' + arbitrary.to_s, + :host_name=>'host' + arbitrary.to_s, + :ssh_user=>'jetstream', + :ssh_password=>'jetstream1!', + :media_location_prefix=>'auxpreview104', + :source_image=>'SUSE__OpenSUSE64121-03192012-en-us-15GB', + :role_size=>'ExtraSmall' + } + end + # ToFix - breaks because it does not refresh each role + # within loop and does not know that it needs to delete + # a deployment instead of a role when it gets down to + # the last role in a deployment + it 'delete everything, build out completely' do + Chef::Log.info 'deleting any existing roles' + @connection.roles.all.each do |role| + Chef::Log.info 'deleting role' + role.name + @connection.roles.delete role.name + break + end + + Chef::Log.info 'deleting any existing hosts' + @connection.hosts.all.each do |host| + Chef::Log.info 'deleting host' + host.name + @connection.hosts.delete host.name + end + + # create 5 new roles + ['001', '002', '003', '004', '005'].each do |val| + arbitrary = rand(1000) + 1 + @params[:role_name]='role' + val + arbitrary.to_s + @params[:host_name]='host' + val + Chef::Log.info 'creating a new role named ' + @params[:role_name] + @connection.deploys.create(@params) + end + + # refresh the roles list + Chef::Log.info 'refreshing roles' + @connection.roles.all + + # list the roles + Chef::Log.info 'display roles' + @connection.roles.roles.each do |role| + Chef::Log.info role.name + end + end + #specify {@connection.roles.exists(@params[:role_name]).should == true} + #specify {@connection.roles.exists(@params[:role_name] + 'notexist').should == false} +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100755 index 0000000..24cbf7e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,40 @@ +#$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +require 'rspec' +require 'equivalent-xml' +require 'chef' +require 'chef/log' + +require 'azure/connection' +require 'azure/rest' +require 'azure/host' +require 'azure/image' +require 'azure/deploy' +require 'azure/role' +require 'azure/disk' + +require 'chef/knife/azure_server_list' +require 'chef/knife/azure_server_delete' +require 'chef/knife/azure_server_create' +require 'chef/knife/azure_server_describe' +require 'chef/knife/azure_image_list' + +def tmpFile filename + tmpdir = 'tmp' + Dir::mkdir tmpdir unless FileTest::directory?(tmpdir) + tmpdir + '/' + filename +end + +Chef::Log.init(tmpFile('debug.log'), 'daily') +Chef::Log.level=:debug + +module AzureSpecHelper + def readFile filename + File.read(File.dirname(__FILE__) + "/unit/assets/#{filename}") + end + + def test_params + params = {:azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + end +end diff --git a/spec/unit/assets/create_deployment.xml b/spec/unit/assets/create_deployment.xml new file mode 100755 index 0000000..3585591 --- /dev/null +++ b/spec/unit/assets/create_deployment.xml @@ -0,0 +1,37 @@ + + unknown_yet + Production + + + + vm01 + + PersistentVMRole + + + LinuxProvisioningConfiguration + myVm + jetstream + jetstream1! + false + + + NetworkConfiguration + + + 22 + SSH + TCP + + + + + + + http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk004Test + SUSE__OpenSUSE64121-03192012-en-us-15GB + + ExtraSmall + + + diff --git a/spec/unit/assets/create_deployment_in_progress.xml b/spec/unit/assets/create_deployment_in_progress.xml new file mode 100755 index 0000000..6d75347 --- /dev/null +++ b/spec/unit/assets/create_deployment_in_progress.xml @@ -0,0 +1 @@ +878c6cd7-73d1-4527-949e-44eb7451547cInProgress \ No newline at end of file diff --git a/spec/unit/assets/create_host.xml b/spec/unit/assets/create_host.xml new file mode 100755 index 0000000..e8eb1ca --- /dev/null +++ b/spec/unit/assets/create_host.xml @@ -0,0 +1,7 @@ + + +service003 + +Explicitly created hosted service +Windows Azure Preview + \ No newline at end of file diff --git a/spec/unit/assets/create_role.xml b/spec/unit/assets/create_role.xml new file mode 100755 index 0000000..a2a4963 --- /dev/null +++ b/spec/unit/assets/create_role.xml @@ -0,0 +1,54 @@ + + + vm01 + + PersistentVMRole + + + LinuxProvisioningConfiguration + myVm + jetstream + jetstream1! + false + + + NetworkConfiguration + + + 22 + SSH + TCP + + + 44 + tcpport_44_myVm + 45 + TCP + + + 55 + tcpport_55_myVm + 55 + TCP + + + 65 + udpport_65_myVm + 65 + UDP + + + 75 + udpport_75_myVm + UDP + + + + + + + http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk004Test + SUSE__OpenSUSE64121-03192012-en-us-15GB + + ExtraSmall + diff --git a/spec/unit/assets/list_deployments_for_service000.xml b/spec/unit/assets/list_deployments_for_service000.xml new file mode 100755 index 0000000..74dc81a --- /dev/null +++ b/spec/unit/assets/list_deployments_for_service000.xml @@ -0,0 +1,126 @@ + + + service003 + Production + 34f75bed486643d39affeb9f98d47227 + Running + + http://service003.cloudapp-preview.net/ + PFNlcnZpY2VDb25maWd1cmF0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL1NlcnZpY2VIb3N0aW5nLzIwMDgvMTAvU2VydmljZUNvbmZpZ3VyYXRpb24iPg0KICA8Um9sZSBuYW1lPSJyb2xlMjA2Ij4NCiAgICA8SW5zdGFuY2VzIGNvdW50PSIxIiAvPg0KICA8L1JvbGU+DQo8L1NlcnZpY2VDb25maWd1cmF0aW9uPg== + + + role206 + role206 + StoppedVM + 0 + 0 + ExtraSmall + + 10.26.198.33 + + + SSH + 65.52.251.57 + 49627 + 22 + tcp + + + tcpport66 + 65.52.251.57 + 66 + 66 + tcp + + + udpport77 + 65.52.251.57 + 77 + 77 + udp + + + udpport88 + 65.52.251.57 + 88 + 88 + udp + + + udpport99 + 65.52.251.57 + 99 + 99 + udp + + + Stopped + + + 1 + + + role206 + + PersistentVMRole + + + NetworkConfiguration + + + 22 + SSH + 49627 + tcp + 65.52.251.57 + + + 66 + tcpport66 + 66 + tcp + 65.52.251.57 + + + 77 + udpport77 + 77 + udp + 65.52.251.57 + + + 88 + udpport88 + 88 + udp + 65.52.251.57 + + + 99 + udpport99 + 99 + udp + 65.52.251.57 + + + + + + + + ReadWrite + service003-role206-0-20120528151407 + http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk_2012_05_28_08_13 + SUSE__OpenSUSE64121-03192012-en-us-15GB.vhd + Linux + + ExtraSmall + + + + false + false + 2012-05-28T15:14:05Z + 2012-05-28T16:29:06Z + + diff --git a/spec/unit/assets/list_deployments_for_service001.xml b/spec/unit/assets/list_deployments_for_service001.xml new file mode 100755 index 0000000..89bf7af --- /dev/null +++ b/spec/unit/assets/list_deployments_for_service001.xml @@ -0,0 +1,166 @@ + + + deployment001 + Production + 2b1f2f0a4b414088a0ec64d583d9c4b3 + Running + + http://service001.cloudapp-preview.net/ + PFNlcnZpY2VDb25maWd1cmF0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL1NlcnZpY2VIb3N0aW5nLzIwMDgvMTAvU2VydmljZUNvbmZpZ3VyYXRpb24iPg0KICA8Um9sZSBuYW1lPSJyb2xlMDAyIj4NCiAgICA8SW5zdGFuY2VzIGNvdW50PSIxIiAvPg0KICA8L1JvbGU+DQogIDxSb2xlIG5hbWU9InJvbGUwMDEiPg0KICAgIDxJbnN0YW5jZXMgY291bnQ9IjEiIC8+DQogIDwvUm9sZT4NCjwvU2VydmljZUNvbmZpZ3VyYXRpb24+ + + + vm002 + vm002 + ReadyRole + 0 + 0 + ExtraSmall + + 10.26.198.146 + + + tcpport66 + 65.52.251.57 + 66 + 66 + tcp + + + SSH + 65.52.249.191 + 22 + 22 + tcp + + + Started + myVm2 + + + role002 + role002 + RoleStateUnknown + 0 + 0 + Small + + 10.26.196.201 + + + ssh + 65.52.249.191 + 23 + 22 + tcp + + + Started + role002 + + + role001 + role001 + ReadyRole + 0 + 0 + Small + + 10.26.196.254 + + + ssh + 65.52.249.191 + 22 + 22 + tcp + + + Started + role001 + + + 1 + + + vm002 + WA-GUEST-OS-1.18_201203-01 + + + NetworkConfiguration + + + 60657 + tcp + 65.52.249.191 + + + + + + + + role002 + WA-GUEST-OS-1.18_201203-01 + PersistentVMRole + + + NetworkConfiguration + + + 22 + ssh + 23 + tcp + 65.52.249.191 + + + + + + + + ReadWrite + deployment001-role002-0-201241722728 + http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk002b + SUSE__OpenSUSE64121-03192012-en-us-15GB + Linux + + Small + + + role001 + WA-GUEST-OS-1.18_201203-01 + PersistentVMRole + + + NetworkConfiguration + + + 22 + ssh + 22 + tcp + 65.52.249.191 + + + + + + + + ReadWrite + deployment001-role001-0-201241722113 + http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk001 + SUSE__OpenSUSE64121-03192012-en-us-15GB + Linux + + Small + + + 1.7 + false + true + 2012-04-17T22:01:10Z + 2012-04-23T23:52:09Z + + diff --git a/spec/unit/assets/list_deployments_for_service002.xml b/spec/unit/assets/list_deployments_for_service002.xml new file mode 100755 index 0000000..f293ab0 --- /dev/null +++ b/spec/unit/assets/list_deployments_for_service002.xml @@ -0,0 +1 @@ +testrequestProduction0f6204c7b38b457a913be856a23e3142Runninghttp://service002.cloudapp-preview.net/PFNlcnZpY2VDb25maWd1cmF0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzZD0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL1NlcnZpY2VIb3N0aW5nLzIwMDgvMTAvU2VydmljZUNvbmZpZ3VyYXRpb24iPg0KICA8Um9sZSBuYW1lPSJ2bTAxIj4NCiAgICA8SW5zdGFuY2VzIGNvdW50PSIxIiAvPg0KICA8L1JvbGU+DQo8L1NlcnZpY2VDb25maWd1cmF0aW9uPg==vm01vm01ReadyRole00ExtraSmall10.26.194.166SSH65.52.251.1445404722tcpStartedmyVm1vm01WA-GUEST-OS-1.18_201203-01PersistentVMRoleNetworkConfiguration22SSH54047tcp65.52.251.144ReadWritetestrequest-vm01-0-2012423214252http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk004TestSUSE__OpenSUSE64121-03192012-en-us-15GBLinuxExtraSmall1.7falsefalse2012-04-23T21:46:44Z2012-04-23T21:56:47Z \ No newline at end of file diff --git a/spec/unit/assets/list_deployments_for_service003.xml b/spec/unit/assets/list_deployments_for_service003.xml new file mode 100755 index 0000000..35c40bd --- /dev/null +++ b/spec/unit/assets/list_deployments_for_service003.xml @@ -0,0 +1 @@ +ResourceNotFoundNo deployments were found. \ No newline at end of file diff --git a/spec/unit/assets/list_disks.xml b/spec/unit/assets/list_disks.xml new file mode 100755 index 0000000..f37f492 --- /dev/null +++ b/spec/unit/assets/list_disks.xml @@ -0,0 +1 @@ +LinuxWindows Azure Preview15http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk104service002-role104-0-201257233812SUSE__OpenSUSE64121-03192012-en-us-15GBLinuxWindows Azure Preview15http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk105service002-role105-0-201257235741SUSE__OpenSUSE64121-03192012-en-us-15GBLinuxWindows Azure Preview30http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk106service002-role106-0-20125801047OpenLogic__OpenLogic-CentOS-62-en-us-30GBLinuxWindows Azure Preview15http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk107service002-role107-0-20125814427SUSE__OpenSUSE64121-03192012-en-us-15GBLinuxWindows Azure Preview15http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk108service002-role108-0-201258152913SUSE__OpenSUSE64121-03192012-en-us-15GBLinuxWindows Azure Preview15http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk109service002-role109-0-201258165947SUSE__OpenSUSE64121-03192012-en-us-15GBservice002service002role119LinuxWindows Azure Preview30http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk119service002-role119-0-201258172212OpenLogic__OpenLogic-CentOS-62-en-us-30GBLinuxWindows Azure Preview30http://auxpreview104imagestore.blob.core.azure-preview.com/os-disks/disk120service002-role120-0-201258203045OpenLogic__OpenLogic-CentOS-62-en-us-30GB \ No newline at end of file diff --git a/spec/unit/assets/list_hosts.xml b/spec/unit/assets/list_hosts.xml new file mode 100755 index 0000000..68e7067 --- /dev/null +++ b/spec/unit/assets/list_hosts.xml @@ -0,0 +1 @@ +https://management-preview.core.windows-int.net/155a9851-88a8-49b4-98e4-58055f08f412/services/hostedservices/service001service001Explicitly created hosted serviceWindows Azure PreviewCreated2012-04-17T21:56:23Z2012-04-17T22:01:08Zhttps://management-preview.core.windows-int.net/155a9851-88a8-49b4-98e4-58055f08f412/services/hostedservices/service002service002Explicitly created hosted serviceWindows Azure PreviewCreated2012-04-17T22:27:08Z2012-04-23T21:42:47Zhttps://management-preview.core.windows-int.net/155a9851-88a8-49b4-98e4-58055f08f412/services/hostedservices/service003service003Explicitly created hosted serviceWindows Azure PreviewCreated2012-04-19T20:17:26Z2012-04-19T20:17:25Z \ No newline at end of file diff --git a/spec/unit/assets/list_images.xml b/spec/unit/assets/list_images.xml new file mode 100755 index 0000000..a20c6c0 --- /dev/null +++ b/spec/unit/assets/list_images.xml @@ -0,0 +1 @@ +CanonicalCANONICAL__Canonical-Ubuntu-12-04-20120519-2012-05-19-en-us-30GB.vhdLinuxhttp://www.ubuntu.com/project/about-ubuntu/licensingUbuntu Server 12.04 (Precise Pangolin) 20120519 Cloud ImageMicrosoft30MSFT__Windows-Server-2008-R2-SP1.11-29-2011Windowshttp://www.microsoft.comMicrosoft Windows Server 2008 R2 SP1Microsoft30MSFT__Windows-Server-2008-R2-SP1-with-SQL-Server-2012-Eval.11-29-2011Windowshttp://download.microsoft.com/download/A/A/7/AA73B8F4-5F4B-4C0B-94F4-FB238C92A916/ENU/SQL Server 2012 Evaluation.rtfMicrosoft Windows 2008 R2 Service Pack 1 with SQL Server 2012 Evaluation Edition (64-bit). This version has licensing restrictions and cannot be used in production. Use of this version is limited to 6 months.Microsoft30MSFT__Windows-Server-8-Beta.en-us.30GB.2012-03-22Windowshttp://msdn.microsoft.com/en-us/windows/apps/br229516The next release of Windows Server, Windows Server "8", offers businesses and hosting providers a scalable, dynamic, and multitenant-aware, cloud-optimized infrastructure. It securely connects across premises and allows IT Professionals to respond to business needs faster and more efficiently.Microsoft40MSFT__Windows-Server-8-Beta.2-17-2012Windowshttp://www.microsoft.comThe next release of Windows Server, Windows Server "8", offers businesses and hosting providers a scalable, dynamic, and multitenant-aware, cloud-optimized infrastructure. It securely connects across premises and allows IT Professionals to respond to business needs faster and more efficiently.Microsoft30MSFT__Windows-Server-2008-R2-SP1.en-us.30GB.2012-3-22Windowshttp://msdn.microsoft.com/en-us/windows/apps/br229516Microsoft Windows 2008 R2 Service Pack 1 for IAASOpenLogicOpenLogic__OpenLogic-CentOS-62-20120509-en-us-30GB.vhdLinuxhttp://www.openlogic.com/azure/service-agreement.phpThis distribution of CentOS version 6.2 is provided by OpenLogic and contains an installation of the Basic Server packages.SUSESUSE__SUSE-Linux-Enterprise-Server-11SP2-20120521-en-us-30GB.vhdLinuxhttp://www.novell.com/licensing/eula/SUSE Linux Enterprise Server is a highly reliable, scalable, and secure server operating system, built to power mission-critical workloads in both physical and virtual environments. It is an affordable, interoperable, and manageable open source foundation. With it, enterprises can cost-effectively deliver core business services, enable secure networks, and simplify the management of their heterogeneous IT infrastructure, maximizing efficiency and value.SUSESUSE__OpenSUSE64121-03192012-en-us-15GB.vhdLinuxhttp://www.novell.com/licensing/eula/OpenSUSE Linux 64 Bits (IAAS M1 Preview) \ No newline at end of file diff --git a/spec/unit/assets/post_success.xml b/spec/unit/assets/post_success.xml new file mode 100755 index 0000000..99abf7a --- /dev/null +++ b/spec/unit/assets/post_success.xml @@ -0,0 +1,6 @@ + + + 878c6cd7-73d1-4527-949e-44eb7451547c + Succeeded + 200 + diff --git a/spec/unit/deploys_list_spec.rb b/spec/unit/deploys_list_spec.rb new file mode 100755 index 0000000..0f81181 --- /dev/null +++ b/spec/unit/deploys_list_spec.rb @@ -0,0 +1,55 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/query_azure_mock') + +describe "deploys" do +include AzureSpecHelper + include QueryAzureMock + before 'setup connection' do + setup_query_azure_mock + end + + specify {@connection.deploys.all.length.should be > 0} + it 'each deployment should have values' do + @connection.deploys.all.each do |deploy| + deploy.name.should_not be_nil + deploy.status.should_not be_nil + deploy.url.should_not be_nil + deploy.roles.length.should be > 0 + end + end + it 'each role should have values' do + @connection.deploys.all.each do |deploy| + #describe_deploy deploy + deploy.roles.each do |role| + #describe_role role + role.name.should_not be_nil + role.status.should_not be_nil + role.size.should_not be_nil + role.ipaddress.should_not be_nil + role.sshport.should_not be_nil + role.sshipaddress.should_not be_nil + end + end + end + def describe_deploy(deploy) + puts '=============================' + puts 'deployed service: ' + deploy.hostedservicename + ' deployment: ' + deploy.name + end + def describe_role(role) + puts 'role: ' + role.name + puts 'status: ' + role.status + puts 'size: ' + role.size + puts 'ip address: ' + role.ipaddress + puts 'ssh port: ' + role.sshport + puts 'ssh ip address: ' + role.sshipaddress + role.tcpports.each do |port| + puts ' tcp: ' + port['Name'] + ' ' + port['Vip'] + ' ' + + port['PublicPort'] + ' ' + port['LocalPort'] + end + role.udpports.each do |port| + puts ' udp: ' + port['Name'] + ' ' + port['Vip'] + ' ' + + port['PublicPort'] + ' ' + port['LocalPort'] + end + puts '=============================' + end +end diff --git a/spec/unit/disks_spec.rb b/spec/unit/disks_spec.rb new file mode 100755 index 0000000..c11b010 --- /dev/null +++ b/spec/unit/disks_spec.rb @@ -0,0 +1,44 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/query_azure_mock') + +describe "disks" do + include AzureSpecHelper + include QueryAzureMock + before 'setup connection' do + setup_query_azure_mock + end + + context 'mock with actually retrieved values' do + it "should find strings" do + items = @connection.disks.all + items.length.should be > 1 + items.each do |disk| + disk.name.should_not be_nil + end + end + it "should contain an attached disk" do + items = @connection.disks.all + count = 0; + items.each do |item| + if item.attached == true + count += 1 + end + end + count.should == 1 + end + it "should contain unattached disks" do + items = @connection.disks.all + count = 0; + items.each do |item| + if item.attached == false + count += 1 + end + end + count.should == 7 + end + it "should clear all unattached disks" do + @connection.disks.clear_unattached + @deletecount.should == 7 + end + end +end diff --git a/spec/unit/hosts_spec.rb b/spec/unit/hosts_spec.rb new file mode 100755 index 0000000..120c889 --- /dev/null +++ b/spec/unit/hosts_spec.rb @@ -0,0 +1,55 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/query_azure_mock') + +describe "hosts" do + include AzureSpecHelper + include QueryAzureMock + before 'setup connection' do + setup_query_azure_mock + end + + context 'get all hosts' do + specify {@connection.hosts.all.length.should be > 1} + it "entry fields should not be nil" do + items = @connection.hosts.all + items.each do |host| + host.name.should_not be_nil + host.url.should_not be_nil + host.label.should_not be_nil + host.dateCreated.should_not be_nil + host.description.should_not be_nil + host.location.should_not be_nil + host.dateModified.should_not be_nil + host.status.should_not be_nil + end + end + specify {@connection.hosts.exists("notExpectedName").should == false} + specify {@connection.hosts.exists("service001").should == true} + end + + context 'create a new host' do + it 'using explicit parameters it should pass in expected body' do + params = {:hosted_service_name=>'service003', 'hosted_service_description'=>'Explicitly created hosted service', 'hosted_service_location'=>'Windows Azure Preview'} + host = @connection.hosts.create(params) + @postname.should == 'hostedservices' + @postverb.should == 'post' + Nokogiri::XML(@postbody).should be_equivalent_to(Nokogiri::XML readFile('create_host.xml')) + end + it 'using default parameters it should pass in expected body' do + params = {:hosted_service_name=>'service003'} + host = @connection.hosts.create(params) + @postname.should == 'hostedservices' + @postverb.should == 'post' + Nokogiri::XML(@postbody).should be_equivalent_to(Nokogiri::XML readFile('create_host.xml')) + end + end + context 'delete a host' do + it 'should pass in correct name, verb, and body' do + @connection.hosts.delete('service001'); + @deletename.should == 'hostedservices/service001' + @deleteverb.should == 'delete' + @deletebody.should == nil + end + end +end + diff --git a/spec/unit/images_spec.rb b/spec/unit/images_spec.rb new file mode 100755 index 0000000..60cf772 --- /dev/null +++ b/spec/unit/images_spec.rb @@ -0,0 +1,35 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/query_azure_mock') + +describe "images" do + include AzureSpecHelper + include QueryAzureMock + before 'setup connection' do + setup_query_azure_mock + end + + context 'mock with actually retrieved values' do + it "should find strings" do + items = @connection.images.all + items.length.should be > 1 + items.each do |image| + image.category.should_not be_nil + image.label.should_not be_nil + image.name.should_not be_nil + image.os.should_not be_nil + image.eula.should_not be_nil + image.description.should_not be_nil + end + end + it "should contain a linux image" do + items = @connection.images.all + foundLinux = false + items.each do |item| + if item.os == 'Linux' + foundLinux = true + end + end + foundLinux.should == true + end + end +end diff --git a/spec/unit/query_azure_mock.rb b/spec/unit/query_azure_mock.rb new file mode 100755 index 0000000..5d1fd16 --- /dev/null +++ b/spec/unit/query_azure_mock.rb @@ -0,0 +1,72 @@ + +module QueryAzureMock + def setup_query_azure_mock + @getname = '' + @getverb = '' + @getbody = '' + + @postname = '' + @postverb = '' + @postbody = '' + + @deletename = '' + @deleteverb = '' + @deletebody = '' + @deletecount = 0 + + params = {:azure_subscription_id => "155a9851-88a8-49b4-98e4-58055f08f412", :azure_pem_file => "AzureLinuxCert.pem", + :azure_host_name => "management-preview.core.windows-int.net", + :service_name => "hostedservices"} + @receivedXML = Nokogiri::XML '' + @connection = Azure::Connection.new(params) + @connection.stub(:query_azure) do |name, verb, body| + Chef::Log.info 'calling web service:' + name + if verb == 'get' || verb == nil + retval = '' + if name == 'images' + retval = Nokogiri::XML readFile('list_images.xml') + elsif name == 'disks' + retval = Nokogiri::XML readFile('list_disks.xml') + elsif name == 'hostedservices' + retval = Nokogiri::XML readFile('list_hosts.xml') + elsif name == 'hostedservices/service001/deploymentslots/Production' + retval = Nokogiri::XML readFile('list_deployments_for_service001.xml') + elsif name == 'hostedservices/service002/deploymentslots/Production' + retval = Nokogiri::XML readFile('list_deployments_for_service002.xml') + elsif name == 'hostedservices/service003/deploymentslots/Production' + retval = Nokogiri::XML readFile('list_deployments_for_service003.xml') + else + Chef::Log.warn 'unknown get value:' + name + end + @getname = name + @getverb = verb + @getbody = body + elsif verb == 'post' + if name == 'hostedservices' + retval = Nokogiri::XML readFile('post_success.xml') + @receivedXML = body + elsif name == 'hostedservices/unknown_yet/deployments' + retval = Nokogiri::XML readFile('post_success.xml') + @receivedXML = body + elsif name == 'hostedservices/service001/deployments/deployment001/roles' + retval = Nokogiri::XML readFile('post_success.xml') + @receivedXML = body + else + Chef::Log.warn 'unknown post value:' + name + end + @postname = name + @postverb = verb + @postbody = body + elsif verb == 'delete' + @deletename = name + @deleteverb = verb + @deletebody = body + @deletecount += 1 + else + Chef::Log.warn 'unknown verb:' + verb + end + retval + end + + end +end diff --git a/spec/unit/roles_create_spec.rb b/spec/unit/roles_create_spec.rb new file mode 100755 index 0000000..189b54f --- /dev/null +++ b/spec/unit/roles_create_spec.rb @@ -0,0 +1,82 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/query_azure_mock') + +describe "roles" do + include AzureSpecHelper + include QueryAzureMock + before do + setup_query_azure_mock + end + context 'delete a role' do + context 'when the role is not the only one in a deployment' do + it 'should pass in correct name, verb, and body' do + @connection.roles.delete('vm002'); + @deletename.should == 'hostedservices/service001/deployments/deployment001/roles/vm002' + @deleteverb.should == 'delete' + @deletebody.should == nil + end + end + end + context 'delete a role' do + context 'when the role is the only one in a deployment' do + it 'should pass in correct name, verb, and body' do + @connection.roles.delete('vm01'); + @deletename.should == 'hostedservices/service002/deployments/testrequest' + @deleteverb.should == 'delete' + @deletebody.should == nil + end + end + end + context 'create a new role' do + it 'should pass in expected body' do + submittedXML=Nokogiri::XML readFile('create_role.xml') + params = { + :hosted_service_name=>'service001', + :role_name=>'vm01', + :host_name=>'myVm', + :ssh_user=>'jetstream', + :ssh_password=>'jetstream1!', + :media_location_prefix=>'auxpreview104', + :os_disk_name=>'disk004Test', + :source_image=>'SUSE__OpenSUSE64121-03192012-en-us-15GB', + :role_size=>'ExtraSmall', + :tcp_endpoints=>'44:45,55:55', + :udp_endpoints=>'65:65,75' + + } + deploy = @connection.deploys.create(params) + #this is a cheesy workaround to make equivalent-xml happy + # write and then re-read the xml + File.open(tmpFile('newRoleRcvd.xml'), 'w') {|f| f.write(@receivedXML) } + File.open(tmpFile('newRoleSbmt.xml'), 'w') {|f| f.write(submittedXML.to_xml) } + rcvd = Nokogiri::XML File.open(tmpFile('newRoleRcvd.xml')) + sbmt = Nokogiri::XML File.open(tmpFile('newRoleSbmt.xml')) + rcvd.should be_equivalent_to(sbmt).respecting_element_order.with_whitespace_intact + end + end + context 'create a new deployment' do + it 'should pass in expected body' do + submittedXML=Nokogiri::XML readFile('create_deployment.xml') + params = { + :hosted_service_name=>'unknown_yet', + :role_name=>'vm01', + :host_name=>'myVm', + :ssh_user=>'jetstream', + :ssh_password=>'jetstream1!', + :media_location_prefix=>'auxpreview104', + :os_disk_name=>'disk004Test', + :source_image=>'SUSE__OpenSUSE64121-03192012-en-us-15GB', + :role_size=>'ExtraSmall' + + } + deploy = @connection.deploys.create(params) + #this is a cheesy workaround to make equivalent-xml happy + # write and then re-read the xml + File.open(tmpFile('newDeployRcvd.xml'), 'w') {|f| f.write(@receivedXML) } + File.open(tmpFile('newDeploySbmt.xml'), 'w') {|f| f.write(submittedXML.to_xml) } + rcvd = Nokogiri::XML File.open(tmpFile('newDeployRcvd.xml')) + sbmt = Nokogiri::XML File.open(tmpFile('newDeploySbmt.xml')) + rcvd.should be_equivalent_to(sbmt).respecting_element_order + end + end +end diff --git a/spec/unit/roles_list_spec.rb b/spec/unit/roles_list_spec.rb new file mode 100755 index 0000000..9bbe057 --- /dev/null +++ b/spec/unit/roles_list_spec.rb @@ -0,0 +1,32 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +require File.expand_path(File.dirname(__FILE__) + '/query_azure_mock') +describe "roles" do + include AzureSpecHelper + include QueryAzureMock + before do + setup_query_azure_mock + end + + it 'show all roles' do + roles = @connection.roles.all + roles.each do |role| + role.name.should_not be_nil + end + roles.length.should == 4 + end + specify {@connection.roles.exists('vm01').should == true} + specify {@connection.roles.exists('vm002').should == true} + specify {@connection.roles.exists('role001').should == true} + specify {@connection.roles.exists('role002').should == true} + specify {@connection.roles.exists('role002qqqqq').should == false} + + it 'each role should have values' do + role = @connection.roles.find('vm01') + role.name.should_not be_nil + role.status.should_not be_nil + role.size.should_not be_nil + role.ipaddress.should_not be_nil + role.sshport.should_not be_nil + role.sshipaddress.should_not be_nil + end +end