diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a92eeeed --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +*.bundle +*.so +*.o +*.a +mkmf.log +gemfiles/*.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ee58d814 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: ruby +sudo: false +script: bundle exec rake test +matrix: + fast_finish: true + include: + - rvm: 1.8.7 + gemfile: gemfiles/Gemfile.1.9.2- + - rvm: 1.9.3 + gemfile: gemfiles/Gemfile.1.9.2+ + - rvm: 2.0.0 + gemfile: Gemfile + - rvm: 2.1.0 + gemfile: Gemfile + - rvm: 2.1.1 + gemfile: Gemfile + - rvm: 2.2.0 + gemfile: Gemfile + - rvm: jruby-18mode + gemfile: gemfiles/Gemfile.1.9.2- + - rvm: jruby-19mode + gemfile: gemfiles/Gemfile.1.9.2+ + - rvm: jruby-head + gemfile: Gemfile + allow_failures: + - rvm: jruby-head +notifications: + email: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8494aeb2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## Getting Involved + +New contributors are always welcome, when it doubt please ask questions. We strive to be an open and welcoming community. Please be nice to one another. + +### Coding + +* Pick a task: +* Offer feedback on open [pull requests](https://github.com/fog/fog-vsphere/pulls). +* Review open [issues](https://github.com/fog/fog-vsphere/issues) for things to help on. +* [Create an issue](https://github.com/fog/fog-vsphere/issues/new) to start a discussion on additions or features. +* Fork the project, add your changes and tests to cover them in a topic branch. +* Commit your changes and rebase against `fog/fog-vsphere` to ensure everything is up to date. +* [Submit a pull request](https://github.com/fog/fog-vsphere/compare/) + +### Non-Coding + +* Offer feedback on open [issues](https://github.com/fog/fog-vsphere/issues). +* Organize or volunteer at events. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 00000000..9276019f --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,57 @@ +* Ahmed Elsabbahy +* Alan Sebastian +* Carl Caum +* Carlos Sanchez +* Chris Thompson +* Chris Thompson +* Cyrus Team +* Darren Foo +* Dominic Cleal +* Eric Stonfer +* Ewoud Kohl van Wijngaarden +* Francois Herbert +* Francois Herbert +* Ivan Nečas +* James Herdman +* Jeff McCune +* Justin Clayton +* Justin Pratt +* Justin Pratt +* Karan Misra +* Kelsey Hightower +* Kevin Menard +* Kevin Menard +* Lance Ivy +* Lukas Zapletal +* Marc Grimme +* Marc Grimme +* Martin Matuska +* Matt Darby +* Matthew Black +* Matthew Black +* Michael Moll +* Mick Pollard +* Ming Jin +* Nick Huanca +* Nick Huanca +* Nick Huanuca +* Ohad Levy +* Ohad Levy +* Oscar Elfving +* Paul Thornthwaite +* Paul Thornthwaite +* Rich Lane +* Samuel Keeley +* Shlomi Zadok +* Simon Josi +* Tejas Ravindra Mandke +* Timur Alperovich +* Wesley Beary +* Wesley Beary +* Xavier Fontrodona +* alan +* endzyme +* geemus +* karmab +* slivik +* tipt0e diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..196ab134 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in fog-vsphere.gemspec +gemspec diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..459d74a0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 [CONTRIBUTORS.md](CONTRIBUTORS.md) + +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/README.md b/README.md new file mode 100644 index 00000000..f1af7d9d --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Fog::Vsphere + +[![Gem Version](https://badge.fury.io/rb/fog-vsphere.svg)](http://badge.fury.io/rb/fog-vsphere) [![Build Status](https://travis-ci.org/fog/fog-vsphere.svg?branch=master)](https://travis-ci.org/fog/fog-vsphere) [![Dependency Status](https://gemnasium.com/fog/fog-vsphere.svg)](https://gemnasium.com/fog/fog-vsphere) [![Coverage Status](https://img.shields.io/coveralls/fog/fog-vsphere.svg)](https://coveralls.io/r/fog/fog-vsphere) [![Code Climate](https://codeclimate.com/github/fog/fog-vsphere.png)](https://codeclimate.com/github/fog/fog-vsphere) [![Stories in Ready](https://badge.waffle.io/fog/fog-vsphere.png?label=ready&title=Ready)](https://waffle.io/fog/fog-vsphere) + +TODO: Write a gem description + +## Installation + +Add this line to your application's Gemfile: + +gem 'fog-vsphere' + +And then execute: + +$ bundle + +Or install it yourself as: + +$ gem install fog-vsphere + +## Usage + +TODO: Write usage instructions here + +## Contributing + +1. Fork it ( https://github.com/fog/fog-vsphere/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..4e09ec38 --- /dev/null +++ b/Rakefile @@ -0,0 +1,15 @@ +require 'bundler/gem_tasks' +# require 'rake/testtask' + +# Rake::TestTask.new do |t| +# t.libs << 'spec/' +# t.test_files = Rake::FileList['spec/**/*_spec.rb'] +# t.verbose = true +# end + +mock = ENV['FOG_MOCK'] || 'true' +task :test do + sh("export FOG_MOCK=#{mock} && bundle exec shindont") +end + +task(:default => [:test]) diff --git a/fog-vsphere.gemspec b/fog-vsphere.gemspec new file mode 100644 index 00000000..065237b7 --- /dev/null +++ b/fog-vsphere.gemspec @@ -0,0 +1,30 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'fog/vsphere/version' + +Gem::Specification.new do |spec| + spec.name = 'fog-vsphere' + spec.version = Fog::Vsphere::VERSION + spec.authors = ['J.R. Garcia'] + spec.email = ['jrg@vmware.com'] + + spec.summary = "Module for the 'fog' gem to support VMware vSphere." + spec.description = 'This library can be used as a module for `fog` or as standalone provider to use the XENSERVER in applications.' + spec.homepage = 'https://github.com/fog/fog-vsphere' + spec.license = 'MIT' + + spec.files = `git ls-files -z`.split("\x0") + spec.test_files = spec.files.grep(%r{^tests\/}) + + spec.require_paths = ['lib'] + + spec.add_runtime_dependency 'fog-core', '~> 1.32' + spec.add_runtime_dependency 'rbvmomi', '~> 1.8' + + spec.add_development_dependency 'bundler', '~> 1.10' + spec.add_development_dependency 'pry', '~> 0.10' + spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rubocop', '~> 0.34' + spec.add_development_dependency 'shindo', '~> 0.3' +end diff --git a/gemfiles/Gemfile.1.9.2+ b/gemfiles/Gemfile.1.9.2+ new file mode 100644 index 00000000..5e8e3f87 --- /dev/null +++ b/gemfiles/Gemfile.1.9.2+ @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gem 'net-ssh', '~> 2.9' +gem 'rubocop', '~> 0.34' + +gemspec :path => '..' diff --git a/gemfiles/Gemfile.1.9.2- b/gemfiles/Gemfile.1.9.2- new file mode 100644 index 00000000..fba6e608 --- /dev/null +++ b/gemfiles/Gemfile.1.9.2- @@ -0,0 +1,10 @@ +source 'https://rubygems.org' + +gem 'mime-types', '~> 1.25' +gem 'net-ssh', '~> 2.9' +gem 'nokogiri', '~> 1.5.11' +gem 'rainbow', '~> 1.99' +gem 'rest-client', '~> 1.6.8' +gem 'rubocop', '~> 0.5.0' + +gemspec :path => '..' diff --git a/lib/fog/vsphere.rb b/lib/fog/vsphere.rb new file mode 100644 index 00000000..cc247ce2 --- /dev/null +++ b/lib/fog/vsphere.rb @@ -0,0 +1,40 @@ +require 'fog/core' +require 'fog/vsphere/compute' + +module Fog + module Vsphere + extend Fog::Provider + + module Errors + class ServiceError < Fog::Errors::Error; end + class SecurityError < ServiceError; end + class NotFound < ServiceError; end + end + + service(:compute, 'Compute') + + # This helper was originally added as Fog.class_as_string and moved to core but only used here + def self.class_from_string classname, defaultpath="" + if classname and classname.is_a? String then + chain = classname.split("::") + klass = Kernel + chain.each do |klass_string| + klass = klass.const_get klass_string + end + if klass.is_a? Class then + klass + elsif defaultpath != nil then + class_from_string((defaultpath.split("::")+chain).join("::"), nil) + else + nil + end + elsif classname and classname.is_a? Class then + classname + else + nil + end + rescue NameError + defaultpath != nil ? class_from_string((defaultpath.split("::")+chain).join("::"), nil) : nil + end + end +end diff --git a/lib/fog/vsphere/compute.rb b/lib/fog/vsphere/compute.rb new file mode 100644 index 00000000..f0284573 --- /dev/null +++ b/lib/fog/vsphere/compute.rb @@ -0,0 +1,465 @@ +require 'digest/sha2' + +module Fog + module Compute + class Vsphere < Fog::Service + requires :vsphere_username, :vsphere_password, :vsphere_server + recognizes :vsphere_port, :vsphere_path, :vsphere_ns + recognizes :vsphere_rev, :vsphere_ssl, :vsphere_expected_pubkey_hash + recognizes :vsphere_debug + + model_path 'fog/vsphere/models/compute' + model :server + collection :servers + model :servertype + collection :servertypes + model :datacenter + collection :datacenters + model :interface + collection :interfaces + model :interfacetype + collection :interfacetypes + model :volume + collection :volumes + model :template + collection :templates + model :cluster + collection :clusters + model :resource_pool + collection :resource_pools + model :network + collection :networks + model :datastore + collection :datastores + model :folder + collection :folders + model :customvalue + collection :customvalues + model :customfield + collection :customfields + model :scsicontroller + + request_path 'fog/vsphere/requests/compute' + request :current_time + request :cloudinit_to_customspec + request :list_virtual_machines + request :vm_power_off + request :vm_power_on + request :vm_reboot + request :vm_clone + request :vm_destroy + request :vm_migrate + request :vm_execute + request :list_datacenters + request :get_datacenter + request :list_clusters + request :get_cluster + request :list_resource_pools + request :get_resource_pool + request :list_networks + request :get_network + request :list_datastores + request :get_datastore + request :list_compute_resources + request :get_compute_resource + request :list_templates + request :get_template + request :get_folder + request :list_folders + request :create_vm + request :list_vm_interfaces + request :modify_vm_interface + request :modify_vm_volume + request :list_vm_volumes + request :get_virtual_machine + request :vm_reconfig_hardware + request :vm_reconfig_memory + request :vm_reconfig_cpus + request :vm_config_vnc + request :create_folder + request :list_server_types + request :get_server_type + request :list_interface_types + request :get_interface_type + request :list_vm_customvalues + request :list_customfields + request :get_vm_first_scsi_controller + request :set_vm_customvalue + + module Shared + attr_reader :vsphere_is_vcenter + attr_reader :vsphere_rev + attr_reader :vsphere_server + attr_reader :vsphere_username + + protected + + ATTR_TO_PROP = { + :id => 'config.instanceUuid', + :name => 'name', + :uuid => 'config.uuid', + :template => 'config.template', + :parent => 'parent', + :hostname => 'summary.guest.hostName', + :operatingsystem => 'summary.guest.guestFullName', + :ipaddress => 'guest.ipAddress', + :power_state => 'runtime.powerState', + :connection_state => 'runtime.connectionState', + :hypervisor => 'runtime.host', + :tools_state => 'guest.toolsStatus', + :tools_version => 'guest.toolsVersionStatus', + :memory_mb => 'config.hardware.memoryMB', + :cpus => 'config.hardware.numCPU', + :corespersocket => 'config.hardware.numCoresPerSocket', + :overall_status => 'overallStatus', + :guest_id => 'config.guestId', + :hardware_version => 'config.version', + :cpuHotAddEnabled => 'config.cpuHotAddEnabled', + :memoryHotAddEnabled => 'config.memoryHotAddEnabled', + :firmware => 'config.firmware', + } + + def convert_vm_view_to_attr_hash(vms) + vms = @connection.serviceContent.propertyCollector.collectMultiple(vms,*ATTR_TO_PROP.values.uniq) + vms.map { |vm| props_to_attr_hash(*vm) } + end + + # Utility method to convert a VMware managed object into an attribute hash. + # This should only really be necessary for the real class. + # This method is expected to be called by the request methods + # in order to massage VMware Managed Object References into Attribute Hashes. + def convert_vm_mob_ref_to_attr_hash(vm_mob_ref) + return nil unless vm_mob_ref + + props = vm_mob_ref.collect!(*ATTR_TO_PROP.values.uniq) + props_to_attr_hash vm_mob_ref, props + end + + def props_to_attr_hash vm_mob_ref, props + # NOTE: Object.tap is in 1.8.7 and later. + # Here we create the hash object that this method returns, but first we need + # to add a few more attributes that require additional calls to the vSphere + # API. The hypervisor name and mac_addresses attributes may not be available + # so we need catch any exceptions thrown during lookup and set them to nil. + # + # The use of the "tap" method here is a convenience, it allows us to update the + # hash object without explicitly returning the hash at the end of the method. + Hash[ATTR_TO_PROP.map { |k,v| [k.to_s, props[v]] }].tap do |attrs| + attrs['id'] ||= vm_mob_ref._ref + attrs['mo_ref'] = vm_mob_ref._ref + # The name method "magically" appears after a VM is ready and + # finished cloning. + if attrs['hypervisor'].kind_of?(RbVmomi::VIM::HostSystem) + host = attrs['hypervisor'] + attrs['datacenter'] = Proc.new { parent_attribute(host.path, :datacenter)[1] rescue nil } + attrs['cluster'] = Proc.new { parent_attribute(host.path, :cluster)[1] rescue nil } + attrs['hypervisor'] = Proc.new { host.name rescue nil } + attrs['resource_pool'] = Proc.new {(vm_mob_ref.resourcePool || host.resourcePool).name rescue nil} + end + # This inline rescue catches any standard error. While a VM is + # cloning, a call to the macs method will throw and NoMethodError + attrs['mac_addresses'] = Proc.new {vm_mob_ref.macs rescue nil} + # Rescue nil to catch testing while vm_mob_ref isn't reaL?? + attrs['path'] = "/"+attrs['parent'].path.map(&:last).join('/') rescue nil + end + end + # returns the parent object based on a type + # provides both real RbVmomi object and its name. + # e.g. + #[Datacenter("datacenter-2"), "dc-name"] + def parent_attribute path, type + element = case type + when :datacenter + RbVmomi::VIM::Datacenter + when :cluster + RbVmomi::VIM::ClusterComputeResource + when :host + RbVmomi::VIM::HostSystem + else + raise "Unknown type" + end + path.select {|x| x[0].is_a? element}.flatten + rescue + nil + end + + # returns vmware managed obj id string + def managed_obj_id obj + obj.to_s.match(/\("([^"]+)"\)/)[1] + end + + def is_uuid?(id) + !(id =~ /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/).nil? + end + end + + class Mock + include Shared + + def self.data + @data ||= Hash.new do |hash, key| + hash[key] = { + :servers => { + "5032c8a5-9c5e-ba7a-3804-832a03e16381" => { + "resource_pool" => "Resources", + "memory_mb" => 2196, + "mac_addresses" => { "Network adapter 1" => "00:50:56:a9:00:28" }, + "power_state" => "poweredOn", + "cpus" => 1, + "hostname" => "dhcp75-197.virt.bos.redhat.com", + "mo_ref" => "vm-562", + "connection_state" => "connected", + "overall_status" => "green", + "datacenter" => "Solutions", + "volumes" => + [{ + "id" => "6000C29c-a47d-4cd9-5249-c371de775f06", + "datastore" => "Storage1", + "mode" => "persistent", + "size" => 8388608, + "thin" => true, + "name" => "Hard disk 1", + "filename" => "[Storage1] rhel6-mfojtik/rhel6-mfojtik.vmdk", + "size_gb" => 8 + }], + "interfaces" => + [{"mac" => "00:50:56:a9:00:28", + "network" => "VM Network", + "name" => "Network adapter 1", + "status" => "ok", + "summary" => "VM Network", + }], + "hypervisor" => "gunab.puppetlabs.lan", + "guest_id" => "rhel6_64Guest", + "tools_state" => "toolsOk", + "cluster" => "Solutionscluster", + "name" => "rhel64", + "operatingsystem" => "Red Hat Enterprise Linux 6 (64-bit)", + "path" => "/Datacenters/Solutions/vm", + "uuid" => "4229f0e9-bfdc-d9a7-7bac-12070772e6dc", + "instance_uuid" => "5032c8a5-9c5e-ba7a-3804-832a03e16381", + "id" => "5032c8a5-9c5e-ba7a-3804-832a03e16381", + "tools_version" => "guestToolsUnmanaged", + "ipaddress" => "192.168.100.184", + "template" => false + }, + "502916a3-b42e-17c7-43ce-b3206e9524dc" => { + "resource_pool" => "Resources", + "memory_mb" => 512, + "power_state" => "poweredOn", + "mac_addresses" => { "Network adapter 1" => "00:50:56:a9:00:00" }, + "hostname" => nil, + "cpus" => 1, + "connection_state" => "connected", + "mo_ref" => "vm-621", + "overall_status" => "green", + "datacenter" => "Solutions", + "volumes" => + [{"thin" => false, + "size_gb" => 10, + "datastore" => "datastore1", + "filename" => "[datastore1] i-1342439683/i-1342439683.vmdk", + "size" => 10485762, + "name" => "Hard disk 1", + "mode" => "persistent", + "id" => "6000C29b-f364-d073-8316-8e98ac0a0eae" }], + "interfaces" => + [{ "summary" => "VM Network", + "mac" => "00:50:56:a9:00:00", + "status" => "ok", + "network" => "VM Network", + "name" => "Network adapter 1" }], + "hypervisor" => "gunab.puppetlabs.lan", + "guest_id" => nil, + "cluster" => "Solutionscluster", + "tools_state" => "toolsNotInstalled", + "name" => "i-1342439683", + "operatingsystem" => nil, + "path" => "/", + "tools_version" => "guestToolsNotInstalled", + "uuid" => "4229e0de-30cb-ceb2-21f9-4d8d8beabb52", + "instance_uuid" => "502916a3-b42e-17c7-43ce-b3206e9524dc", + "id" => "502916a3-b42e-17c7-43ce-b3206e9524dc", + "ipaddress" => nil, + "template" => false + }, + "5029c440-85ee-c2a1-e9dd-b63e39364603" => { + "resource_pool" => "Resources", + "memory_mb" => 2196, + "power_state" => "poweredOn", + "mac_addresses" => { "Network adapter 1" => "00:50:56:b2:00:af" }, + "hostname" => "centos56gm.localdomain", + "cpus" => 1, + "connection_state" => "connected", + "mo_ref" => "vm-715", + "overall_status" => "green", + "datacenter" => "Solutions", + "hypervisor" => "gunab.puppetlabs.lan", + "guest_id" => "rhel6_64Guest", + "cluster" => "Solutionscluster", + "tools_state" => "toolsOk", + "name" => "jefftest", + "operatingsystem" => "Red Hat Enterprise Linux 6 (64-bit)", + "path" => "/", + "tools_version" => "guestToolsUnmanaged", + "ipaddress" => "192.168.100.187", + "uuid" => "42329da7-e8ab-29ec-1892-d6a4a964912a", + "instance_uuid" => "5029c440-85ee-c2a1-e9dd-b63e39364603", + "id" => "5029c440-85ee-c2a1-e9dd-b63e39364603", + "template" => false + } + }, + :datacenters => { + "Solutions" => {:name => "Solutions", :status => "grey"} + }, + :clusters => + [{:id => "1d4d9a3f-e4e8-4c40-b7fc-263850068fa4", + :name => "Solutionscluster", + :num_host => "4", + :num_cpu_cores => "16", + :overall_status => "green", + :datacenter => "Solutions", + :klass => "RbVmomi::VIM::ComputeResource" + }, + {:id => "e4195973-102b-4096-bbd6-5429ff0b35c9", + :name => "Problemscluster", + :num_host => "4", + :num_cpu_cores => "32", + :overall_status => "green", + :datacenter => "Solutions", + :klass => "RbVmomi::VIM::ComputeResource" + }, + { + :klass => "RbVmomi::VIM::Folder", + :clusters => [{:id => "03616b8d-b707-41fd-b3b5-The first", + :name => "Problemscluster", + :num_host => "4", + :num_cpu_cores => "32", + :overall_status => "green", + :datacenter => "Solutions", + :klass => "RbVmomi::VIM::ComputeResource" + }, + {:id => "03616b8d-b707-41fd-b3b5-the Second", + :name => "Lastcluster", + :num_host => "8", + :num_cpu_cores => "32", + :overall_status => "green", + :datacenter => "Solutions", + :klass => "RbVmomi::VIM::ComputeResource"} + ] + } + ] + } + end + end + + def initialize(options={}) + require 'rbvmomi' + @vsphere_username = options[:vsphere_username] + @vsphere_password = 'REDACTED' + @vsphere_server = options[:vsphere_server] + @vsphere_expected_pubkey_hash = options[:vsphere_expected_pubkey_hash] + @vsphere_is_vcenter = true + @vsphere_rev = '4.0' + end + + def data + self.class.data[@vsphere_username] + end + + def reset_data + self.class.data.delete(@vsphere_username) + end + end + + class Real + include Shared + + def initialize(options={}) + require 'rbvmomi' + @vsphere_username = options[:vsphere_username] + @vsphere_password = options[:vsphere_password] + @vsphere_server = options[:vsphere_server] + @vsphere_port = options[:vsphere_port] || 443 + @vsphere_path = options[:vsphere_path] || '/sdk' + @vsphere_ns = options[:vsphere_ns] || 'urn:vim25' + @vsphere_rev = options[:vsphere_rev] || '4.0' + @vsphere_ssl = options[:vsphere_ssl] || true + @vsphere_debug = options[:vsphere_debug] || false + @vsphere_expected_pubkey_hash = options[:vsphere_expected_pubkey_hash] + @vsphere_must_reauthenticate = false + @vsphere_is_vcenter = nil + @connection = nil + connect + negotiate_revision(options[:vsphere_rev]) + authenticate + end + + def reload + connect + # Check if the negotiation was ever run + if @vsphere_is_vcenter.nil? + negotiate + end + authenticate + end + + private + def negotiate_revision(revision = nil) + # Negotiate the API revision + if not revision + rev = @connection.serviceContent.about.apiVersion + @connection.rev = [ rev, ENV['FOG_VSPHERE_REV'] || '4.1' ].min + end + + @vsphere_is_vcenter = @connection.serviceContent.about.apiType == "VirtualCenter" + @vsphere_rev = @connection.rev + end + + def connect + # This is a state variable to allow digest validation of the SSL cert + bad_cert = false + loop do + begin + @connection = RbVmomi::VIM.new :host => @vsphere_server, + :port => @vsphere_port, + :path => @vsphere_path, + :ns => @vsphere_ns, + :rev => @vsphere_rev, + :ssl => @vsphere_ssl, + :insecure => bad_cert, + :debug => @vsphere_debug + break + rescue OpenSSL::SSL::SSLError + raise if bad_cert + bad_cert = true + end + end + + if bad_cert then + validate_ssl_connection + end + end + + def authenticate + begin + @connection.serviceContent.sessionManager.Login :userName => @vsphere_username, + :password => @vsphere_password + rescue RbVmomi::VIM::InvalidLogin => e + raise Fog::Vsphere::Errors::ServiceError, e.message + end + end + + # Verify a SSL certificate based on the hashed public key + def validate_ssl_connection + pubkey = @connection.http.peer_cert.public_key + pubkey_hash = Digest::SHA2.hexdigest(pubkey.to_s) + expected_pubkey_hash = @vsphere_expected_pubkey_hash + if pubkey_hash != expected_pubkey_hash then + raise Fog::Vsphere::Errors::SecurityError, "The remote system presented a public key with hash #{pubkey_hash} but we're expecting a hash of #{expected_pubkey_hash || ''}. If you are sure the remote system is authentic set vsphere_expected_pubkey_hash: in ~/.fog" + end + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/cluster.rb b/lib/fog/vsphere/models/compute/cluster.rb new file mode 100644 index 00000000..f628bf0e --- /dev/null +++ b/lib/fog/vsphere/models/compute/cluster.rb @@ -0,0 +1,28 @@ +module Fog + module Compute + class Vsphere + class Cluster < Fog::Model + identity :id + + attribute :name + attribute :datacenter + attribute :num_host + attribute :num_cpu_cores + attribute :overall_status + attribute :full_path + + def resource_pools(filters = { }) + self.attributes[:resource_pools] ||= id.nil? ? [] : service.resource_pools({ + :service => service, + :cluster => name, + :datacenter => datacenter + }.merge(filters)) + end + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/clusters.rb b/lib/fog/vsphere/models/compute/clusters.rb new file mode 100644 index 00000000..440b47a6 --- /dev/null +++ b/lib/fog/vsphere/models/compute/clusters.rb @@ -0,0 +1,23 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/cluster' + +module Fog + module Compute + class Vsphere + class Clusters < Fog::Collection + model Fog::Compute::Vsphere::Cluster + attr_accessor :datacenter + + def all(filters = {}) + requires :datacenter + load service.list_clusters(filters.merge(:datacenter => datacenter)) + end + + def get(id) + requires :datacenter + new service.get_cluster(id, datacenter) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/customfield.rb b/lib/fog/vsphere/models/compute/customfield.rb new file mode 100644 index 00000000..83bde144 --- /dev/null +++ b/lib/fog/vsphere/models/compute/customfield.rb @@ -0,0 +1,16 @@ +module Fog + module Compute + class Vsphere + class Customfield < Fog::Model + identity :key + + attribute :name + attribute :type + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/customfields.rb b/lib/fog/vsphere/models/compute/customfields.rb new file mode 100644 index 00000000..f2341ead --- /dev/null +++ b/lib/fog/vsphere/models/compute/customfields.rb @@ -0,0 +1,24 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/customfield' + +module Fog + module Compute + class Vsphere + class Customfields < Fog::Collection + model Fog::Compute::Vsphere::Customfield + + attr_accessor :vm + + def all(filters = {}) + load service.list_customfields() + end + + def get(key) + load(service.list_customfields()).find do | cv | + cv.key == ((key.is_a? String) ? key.to_i : key) + end + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/customvalue.rb b/lib/fog/vsphere/models/compute/customvalue.rb new file mode 100644 index 00000000..399c0c44 --- /dev/null +++ b/lib/fog/vsphere/models/compute/customvalue.rb @@ -0,0 +1,14 @@ +module Fog + module Compute + class Vsphere + class Customvalue < Fog::Model + attribute :value + attribute :key + + def to_s + value + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/customvalues.rb b/lib/fog/vsphere/models/compute/customvalues.rb new file mode 100644 index 00000000..fbec7417 --- /dev/null +++ b/lib/fog/vsphere/models/compute/customvalues.rb @@ -0,0 +1,34 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/customvalue' + +module Fog + module Compute + class Vsphere + class Customvalues < Fog::Collection + model Fog::Compute::Vsphere::Customvalue + + attr_accessor :vm + + def all(filters = {}) + requires :vm + case vm + when Fog::Compute::Vsphere::Server + load service.list_vm_customvalues(vm.id) + else + raise 'customvalues should have vm' + end + end + + def get(key) + requires :vm + case vm + when Fog::Compute::Vsphere::Server + load service.list_vm_customvalues(vm.id) + else + raise 'customvalues should have vm' + end.find { | cv | cv.key == key } + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/datacenter.rb b/lib/fog/vsphere/models/compute/datacenter.rb new file mode 100644 index 00000000..26b7d6cb --- /dev/null +++ b/lib/fog/vsphere/models/compute/datacenter.rb @@ -0,0 +1,44 @@ +module Fog + module Compute + class Vsphere + class Datacenter < Fog::Model + identity :id + attribute :name + attribute :path + attribute :status + + def clusters filters = { } + service.clusters({ :datacenter => path.join("/") }.merge(filters)) + end + + def networks filters = { } + service.networks({ :datacenter => path.join("/") }.merge(filters)) + end + + def datastores filters = { } + service.datastores({ :datacenter => path.join("/") }.merge(filters)) + end + + def vm_folders filters = { } + service.folders({ :datacenter => path.join("/"), :type => :vm }.merge(filters)) + end + + def virtual_machines filters = {} + service.servers({ :datacenter => path.join("/") }.merge(filters)) + end + + def servertypes filters={} + service.servertypes({:datacenter => name }.merge(filters)) + end + + def customfields filters = {} + service.customfields({ :datacenter => path.join("/")}.merge(filters)) + end + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/datacenters.rb b/lib/fog/vsphere/models/compute/datacenters.rb new file mode 100644 index 00000000..c5812bc3 --- /dev/null +++ b/lib/fog/vsphere/models/compute/datacenters.rb @@ -0,0 +1,20 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/datacenter' + +module Fog + module Compute + class Vsphere + class Datacenters < Fog::Collection + model Fog::Compute::Vsphere::Datacenter + + def all(filters = {}) + load service.list_datacenters(filters) + end + + def get(name) + new service.get_datacenter(name) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/datastore.rb b/lib/fog/vsphere/models/compute/datastore.rb new file mode 100644 index 00000000..6baa2210 --- /dev/null +++ b/lib/fog/vsphere/models/compute/datastore.rb @@ -0,0 +1,21 @@ +module Fog + module Compute + class Vsphere + class Datastore < Fog::Model + identity :id + + attribute :name + attribute :datacenter + attribute :type + attribute :freespace + attribute :accessible # reachable by at least one hypervisor + attribute :capacity + attribute :uncommitted + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/datastores.rb b/lib/fog/vsphere/models/compute/datastores.rb new file mode 100644 index 00000000..67bbcd00 --- /dev/null +++ b/lib/fog/vsphere/models/compute/datastores.rb @@ -0,0 +1,22 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/datastore' + +module Fog + module Compute + class Vsphere + class Datastores < Fog::Collection + model Fog::Compute::Vsphere::Datastore + attr_accessor :datacenter + + def all(filters = {}) + load service.list_datastores(filters.merge(:datacenter => datacenter)) + end + + def get(id) + requires :datacenter + new service.get_datastore(id, datacenter) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/folder.rb b/lib/fog/vsphere/models/compute/folder.rb new file mode 100644 index 00000000..7dea57e8 --- /dev/null +++ b/lib/fog/vsphere/models/compute/folder.rb @@ -0,0 +1,24 @@ +module Fog + module Compute + class Vsphere + class Folder < Fog::Model + identity :id + + attribute :name + attribute :parent + attribute :datacenter + attribute :path + attribute :type + + def vms + return [] if type.to_s != 'vm' + service.servers(:folder => path, :datacenter => datacenter) + end + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/folders.rb b/lib/fog/vsphere/models/compute/folders.rb new file mode 100644 index 00000000..467bb78a --- /dev/null +++ b/lib/fog/vsphere/models/compute/folders.rb @@ -0,0 +1,24 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/folder' + +module Fog + module Compute + class Vsphere + class Folders < Fog::Collection + model Fog::Compute::Vsphere::Folder + attr_accessor :datacenter, :type, :path + + def all(filters = { }) + requires :datacenter + requires :type + load service.list_folders(filters.merge(:datacenter => datacenter, :type => type, :path => path)) + end + + def get(id) + requires :datacenter + new service.get_folder(id, datacenter, type) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/interface.rb b/lib/fog/vsphere/models/compute/interface.rb new file mode 100644 index 00000000..198501a5 --- /dev/null +++ b/lib/fog/vsphere/models/compute/interface.rb @@ -0,0 +1,91 @@ +module Fog + module Compute + class Vsphere + class Interface < Fog::Model + SAVE_MUTEX = Mutex.new + + identity :mac + alias_method :id, :mac + + attribute :network + attribute :name + attribute :status + attribute :summary + attribute :type + attribute :key + attribute :virtualswitch + attribute :server_id + + def initialize(attributes = {}) + # Assign server first to prevent race condition with persisted? + self.server_id = attributes.delete(:server_id) + + if attributes.key? :type then + if attributes[:type].is_a? String then + attributes[:type] = Fog::Vsphere.class_from_string(attributes[:type], "RbVmomi::VIM") + end + else + attributes[:type] = Fog::Vsphere.class_from_string("VirtualE1000", "RbVmomi::VIM") + end + + super defaults.merge(attributes) + end + + def to_s + name + end + + def server + requires :server_id + service.servers.get(server_id) + end + + def destroy + requires :server_id, :key, :type + + service.destroy_vm_interface(server_id, :key => key, :type => type) + end + + def save + raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted? + requires :server_id, :type, :network + + # Our approach of finding the newly created interface is rough. We assume that the :key value always increments + # and thus the highest :key value must correspond to the created interface. Since this has an inherent race + # condition we need to gate the saves. + SAVE_MUTEX.synchronize do + data = service.add_vm_interface(server_id, attributes) + + if data['task_state'] == 'success' + # We have to query vSphere to get the interface attributes since the task handle doesn't include that info. + created = server.interfaces.all.sort_by(&:key).last + + self.mac = created.mac + self.name = created.name + self.status = created.status + self.summary = created.summary + self.key = created.key + self.virtualswitch = created.virtualswitch + + true + else + false + end + end + end + + private + + def defaults + default_type=Fog.credentials[:default_nic_type] || RbVmomi::VIM::VirtualE1000 + { + :name=>"Network adapter", + :network=>"VM Network", + :summary=>"VM Network", + :type=> Fog::Vsphere.class_from_string(default_type, "RbVmomi::VIM"), + } + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/interfaces.rb b/lib/fog/vsphere/models/compute/interfaces.rb new file mode 100644 index 00000000..04d3dbf5 --- /dev/null +++ b/lib/fog/vsphere/models/compute/interfaces.rb @@ -0,0 +1,67 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/interface' + +module Fog + module Compute + class Vsphere + class Interfaces < Fog::Collection + model Fog::Compute::Vsphere::Interface + + attribute :server_id + + def all(filters = {}) + requires :server_id + + case server + when Fog::Compute::Vsphere::Server + load service.list_vm_interfaces(server.id) + when Fog::Compute::Vsphere::Template + load service.list_template_interfaces(server.id) + else + raise 'interfaces should have vm or template' + end + + self.each { |interface| interface.server_id = server.id } + self + end + + def get(id) + requires :server_id + + case server + when Fog::Compute::Vsphere::Server + interface = service.get_vm_interface(server.id, :key => id, :mac=> id, :name => id) + when Fog::Compute::Vsphere::Template + interface = service.get_template_interfaces(server.id, :key => id, :mac=> id, :name => id) + else + + raise 'interfaces should have vm or template' + end + + if interface + Fog::Compute::Vsphere::Interface.new(interface.merge(:server_id => server.id, :service => service)) + else + nil + end + end + + def new(attributes = {}) + if server_id + super({ :server_id => server_id }.merge(attributes)) + else + super + end + end + + def server + return nil if server_id.nil? + service.servers.get(server_id) + end + + def server=(new_server) + server_id = new_server.id + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/interfacetype.rb b/lib/fog/vsphere/models/compute/interfacetype.rb new file mode 100644 index 00000000..66f1b3fb --- /dev/null +++ b/lib/fog/vsphere/models/compute/interfacetype.rb @@ -0,0 +1,22 @@ +module Fog + module Compute + class Vsphere + class Interfacetype < Fog::Model + identity :id + +# attribute :class + attribute :name + attribute :datacenter + attribute :servertype + + def initialize(attributes={} ) + super attributes + end + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/interfacetypes.rb b/lib/fog/vsphere/models/compute/interfacetypes.rb new file mode 100644 index 00000000..a5365b45 --- /dev/null +++ b/lib/fog/vsphere/models/compute/interfacetypes.rb @@ -0,0 +1,35 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/interfacetype' + +module Fog + module Compute + class Vsphere + class Interfacetypes < Fog::Collection + model Fog::Compute::Vsphere::Interfacetype + attr_accessor :datacenter + attr_accessor :servertype + + def all(filters = { }) + requires :servertype + case servertype + when Fog::Compute::Vsphere::Servertype + load service.list_interface_types(filters.merge({ + :datacenter => datacenter, + :servertype => servertype.id + })) + else + raise 'interfacetypes should have a servertype' + end + end + + def get(id) + requires :servertype + requires :datacenter + new service.get_interface_type id, servertype, datacenter + rescue Fog::Compute::Vsphere::NotFound + nil + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/network.rb b/lib/fog/vsphere/models/compute/network.rb new file mode 100644 index 00000000..fcd7eca5 --- /dev/null +++ b/lib/fog/vsphere/models/compute/network.rb @@ -0,0 +1,18 @@ +module Fog + module Compute + class Vsphere + class Network < Fog::Model + identity :id + + attribute :name + attribute :datacenter + attribute :accessible # reachable by at least one hypervisor + attribute :virtualswitch + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/networks.rb b/lib/fog/vsphere/models/compute/networks.rb new file mode 100644 index 00000000..c727cb9d --- /dev/null +++ b/lib/fog/vsphere/models/compute/networks.rb @@ -0,0 +1,23 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/network' + +module Fog + module Compute + class Vsphere + class Networks < Fog::Collection + model Fog::Compute::Vsphere::Network + attr_accessor :datacenter + + def all(filters = {}) + f = { :datacenter => datacenter }.merge(filters) + load service.list_networks(f) + end + + def get(id) + requires :datacenter + new service.get_network(id, datacenter) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/resource_pool.rb b/lib/fog/vsphere/models/compute/resource_pool.rb new file mode 100644 index 00000000..d5f1b916 --- /dev/null +++ b/lib/fog/vsphere/models/compute/resource_pool.rb @@ -0,0 +1,19 @@ +module Fog + module Compute + class Vsphere + class ResourcePool < Fog::Model + identity :id + + attribute :name + attribute :cluster + attribute :datacenter + attribute :configured_memory_mb + attribute :overall_status + + def to_s + name + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/resource_pools.rb b/lib/fog/vsphere/models/compute/resource_pools.rb new file mode 100644 index 00000000..4407eba3 --- /dev/null +++ b/lib/fog/vsphere/models/compute/resource_pools.rb @@ -0,0 +1,23 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/resource_pool' + +module Fog + module Compute + class Vsphere + class ResourcePools < Fog::Collection + model Fog::Compute::Vsphere::ResourcePool + attr_accessor :datacenter, :cluster + + def all(filters = {}) + load service.list_resource_pools(filters.merge(:datacenter => datacenter, :cluster => cluster)) + end + + def get(id) + requires :datacenter + requires :cluster + new service.get_resource_pool(id, cluster, datacenter) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/scsicontroller.rb b/lib/fog/vsphere/models/compute/scsicontroller.rb new file mode 100644 index 00000000..6a7fed76 --- /dev/null +++ b/lib/fog/vsphere/models/compute/scsicontroller.rb @@ -0,0 +1,16 @@ +module Fog + module Compute + class Vsphere + class SCSIController < Fog::Model + attribute :shared_bus + attribute :type + attribute :unit_number + attribute :key + + def to_s + "#{type} ##{key}: shared: #{shared_bus}, unit_number: #{unit_number}" + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/server.rb b/lib/fog/vsphere/models/compute/server.rb new file mode 100644 index 00000000..559e3358 --- /dev/null +++ b/lib/fog/vsphere/models/compute/server.rb @@ -0,0 +1,296 @@ +require 'fog/compute/models/server' + +module Fog + module Compute + class Vsphere + class Server < Fog::Compute::Server + extend Fog::Deprecation + deprecate(:ipaddress, :public_ip_address) + + # This will be the instance uuid which is globally unique across + # a vSphere deployment. + identity :id + + # JJM REVISIT (Extend the model of a vmware server) + # SEE: http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.VirtualMachine.html + # (Take note of the See also section.) + # In particular: + # GuestInfo: information about the guest operating system + # VirtualMachineConfigInfo: Access to the VMX file and configuration + + attribute :name + # UUID may be the same from VM to VM if the user does not select (I copied it) + attribute :uuid + attribute :hostname + attribute :operatingsystem + attribute :public_ip_address, :aliases => 'ipaddress' + attribute :power_state, :aliases => 'power' + attribute :tools_state, :aliases => 'tools' + attribute :tools_version + attribute :mac_addresses, :aliases => 'macs' + attribute :hypervisor, :aliases => 'host' + attribute :connection_state + attribute :mo_ref + attribute :path + attribute :memory_mb + attribute :cpus + attribute :corespersocket + attribute :interfaces + attribute :volumes + attribute :customvalues + attribute :overall_status, :aliases => 'status' + attribute :cluster + attribute :datacenter + attribute :resource_pool + attribute :instance_uuid # move this --> id + attribute :guest_id + attribute :hardware_version + attribute :scsi_controller # this is the first scsi controller. Right now no more of them can be used. + attribute :cpuHotAddEnabled + attribute :memoryHotAddEnabled + attribute :firmware + + def initialize(attributes={} ) + super defaults.merge(attributes) + self.instance_uuid ||= id # TODO: remvoe instance_uuid as it can be replaced with simple id + initialize_interfaces + initialize_volumes + initialize_customvalues + initialize_scsi_controller + end + + # Lazy Loaded Attributes + [:datacenter, :cluster, :hypervisor, :resource_pool, :mac_addresses].each do |attr| + define_method attr do + attributes[attr] = attributes[attr].call if attributes[attr].is_a?(Proc) + attributes[attr] + end + end + # End Lazy Loaded Attributes + + def vm_reconfig_memory(options = {}) + requires :instance_uuid, :memory + service.vm_reconfig_memory('instance_uuid' => instance_uuid, 'memory' => memory_mb) + end + + def vm_reconfig_cpus(options = {}) + requires :instance_uuid, :cpus, :corespersocket + service.vm_reconfig_cpus('instance_uuid' => instance_uuid, 'cpus' => cpus, 'corespersocket' => corespersocket) + end + + def vm_reconfig_hardware(hardware_spec, options = {}) + requires :instance_uuid + service.vm_reconfig_hardware('instance_uuid' => instance_uuid, 'hardware_spec' => hardware_spec) + end + + def start(options = {}) + requires :instance_uuid + service.vm_power_on('instance_uuid' => instance_uuid) + end + + def stop(options = {}) + options = { :force => !tools_installed? || !tools_running? }.merge(options) + requires :instance_uuid + service.vm_power_off('instance_uuid' => instance_uuid, 'force' => options[:force]) + end + + def reboot(options = {}) + options = { :force => false }.merge(options) + requires :instance_uuid + service.vm_reboot('instance_uuid' => instance_uuid, 'force' => options[:force]) + end + + def destroy(options = {}) + requires :instance_uuid + if ready? + # need to turn it off before destroying + stop(options) + wait_for { !ready? } + end + service.vm_destroy('instance_uuid' => instance_uuid) + end + + def migrate(options = {}) + options = { :priority => 'defaultPriority' }.merge(options) + requires :instance_uuid + service.vm_migrate('instance_uuid' => instance_uuid, 'priority' => options[:priority]) + end + + # Clone from a server object + # + # ==== Parameters + # *<~Hash>: + # * 'name'<~String> - *REQUIRED* Name of the _new_ VirtualMachine + # * See more options in vm_clone request/compute/vm_clone.rb + # + def clone(options = {}) + requires :name, :datacenter, :path + + # Convert symbols to strings + req_options = options.reduce({}) { |hsh, (k,v)| hsh[k.to_s] = v; hsh } + + # Give our path to the request + req_options['template_path'] ="#{relative_path}/#{name}" + req_options['datacenter'] = "#{datacenter}" + + # Perform the actual clone + clone_results = service.vm_clone(req_options) + + # We need to assign the service, otherwise we can't reload the model + # Create the new VM model. TODO This only works when "wait=true" + new_vm = self.class.new(clone_results['new_vm'].merge(:service => self.service)) + + # We need to assign the collection otherwise we + # cannot reload the model. + new_vm.collection = self.collection + + # Return the new VM model. + new_vm + end + + def ready? + power_state == "poweredOn" + end + + def tools_installed? + tools_state != "toolsNotInstalled" + end + + def tools_running? + ["toolsOk","toolsOld"].include? tools_state + end + + # defines VNC attributes on the hypervisor + def config_vnc(options = {}) + requires :instance_uuid + service.vm_config_vnc(options.merge('instance_uuid' => instance_uuid)) + end + + # returns a hash of VNC attributes required for service + def vnc + requires :instance_uuid + service.vm_get_vnc(instance_uuid) + end + + def memory + memory_mb * 1024 * 1024 + end + + def sockets + cpus / corespersocket + end + + def mac + interfaces.first.mac unless interfaces.empty? + end + + def interfaces + attributes[:interfaces] ||= id.nil? ? [] : service.interfaces( :server_id => self.id ) + end + + def interface_ready? attrs + (attrs.is_a? Hash and attrs[:blocking]) or attrs.is_a? Fog::Compute::Vsphere::Interface + end + + def add_interface attrs + Fog::Logger.deprecation(".add_interface is deprecated. Call .interfaces.create instead.") + + interfaces.create(attrs) + end + + def update_interface attrs + wait_for { not ready? } if interface_ready? attrs + service.update_vm_interface(id, attrs) + end + + def destroy_interface attrs + Fog::Logger.deprecation(".destroy_vm_interface is deprecated. Call .interfaces.get(:key => ).destroy instead.") + + interfaces.get(attrs[:key] || attrs['key']).destroy + end + + def volumes + attributes[:volumes] ||= id.nil? ? [] : service.volumes(:server_id => self.id) + end + + def customvalues + attributes[:customvalues] ||= id.nil? ? [] : service.customvalues( :vm => self ) + end + + def scsi_controller + self.attributes[:scsi_controller] ||= service.get_vm_first_scsi_controller(id) + end + + def folder + return nil unless datacenter and path + attributes[:folder] ||= service.folders(:datacenter => datacenter, :type => :vm).get(path) + end + + def save + requires :name, :cluster, :datacenter + if persisted? + raise "update is not supported yet" + # service.update_vm(attributes) + else + self.id = service.create_vm(attributes) + end + reload + end + + def new? + id.nil? + end + + def reload + # reload does not re-read assoiciated attributes, so we clear it manually + [:interfaces, :volumes].each do |attr| + self.attributes.delete(attr) + end + super + end + + def relative_path + requires :path, :datacenter + + (path.split('/').reject {|e| e.empty?} - ["Datacenters", datacenter, "vm"]).join("/") + end + + private + + def defaults + { + :cpus => 1, +# :corespersocket => 1, + :memory_mb => 512, + :guest_id => 'otherGuest', + :path => '/' + } + end + + def initialize_interfaces + if attributes[:interfaces] and attributes[:interfaces].is_a?(Array) + self.attributes[:interfaces].map! { |nic| nic.is_a?(Hash) ? service.interfaces.new(nic) : nic } + end + end + + def initialize_volumes + if attributes[:volumes] and attributes[:volumes].is_a?(Array) + self.attributes[:volumes].map! { |vol| vol.is_a?(Hash) ? service.volumes.new(vol) : vol } + end + end + + def initialize_customvalues + if attributes[:customvalues] and attributes[:customvalues].is_a?(Array) + self.attributes[:customvalues].map { |cfield| cfield.is_a?(Hash) ? service.customvalue.new(cfield) : cfield} + end + end + + def initialize_scsi_controller + if attributes[:scsi_controller] and attributes[:scsi_controller].is_a?(Hash) + Fog::Compute::Vsphere::SCSIController.new(self.attributes[:scsi_controller]) + end + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/servers.rb b/lib/fog/vsphere/models/compute/servers.rb new file mode 100644 index 00000000..7c7b9f1a --- /dev/null +++ b/lib/fog/vsphere/models/compute/servers.rb @@ -0,0 +1,37 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/server' + +module Fog + module Compute + class Vsphere + class Servers < Fog::Collection + model Fog::Compute::Vsphere::Server + attr_accessor :datacenter + attr_accessor :network + attr_accessor :cluster + attr_accessor :resource_pool + attr_accessor :folder + + # 'folder' => '/Datacenters/vm/Jeff/Templates' will be MUCH faster. + # than simply listing everything. + def all(filters = { }) + f = { + :datacenter => datacenter, + :cluster => cluster, + :network => network, + :resource_pool => resource_pool, + :folder => folder + }.merge(filters) + + load service.list_virtual_machines(f) + end + + def get(id, datacenter = nil) + new service.get_virtual_machine id, datacenter + rescue Fog::Compute::Vsphere::NotFound + nil + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/servertype.rb b/lib/fog/vsphere/models/compute/servertype.rb new file mode 100644 index 00000000..6bc0665f --- /dev/null +++ b/lib/fog/vsphere/models/compute/servertype.rb @@ -0,0 +1,36 @@ +module Fog + module Compute + class Vsphere + class Servertype < Fog::Model + identity :id + + attribute :family + attribute :fullname + attribute :datacenter + attribute :interfacetypes + + def initialize(attributes={} ) + super defaults.merge(attributes) + end + + def to_s + id + end + + def interfacetypes filters={} + attributes[:interfacetypes] ||= service.interfacetypes({ :datacenter => datacenter, :servertype => self }.merge(filters)) + end + + private + + def defaults + { + :id=>"otherGuest64", + :family=>"otherGuestFamily", + :interfacetypes => nil, + } + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/servertypes.rb b/lib/fog/vsphere/models/compute/servertypes.rb new file mode 100644 index 00000000..d3f04536 --- /dev/null +++ b/lib/fog/vsphere/models/compute/servertypes.rb @@ -0,0 +1,24 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/servertype' + +module Fog + module Compute + class Vsphere + class Servertypes < Fog::Collection + model Fog::Compute::Vsphere::Servertype + attr_accessor :datacenter, :id, :fullname + + def all(filters = { }) + load service.list_server_types(filters.merge({:datacenter => datacenter})) + end + + def get(id) + requires :datacenter + new service.get_server_type(id, datacenter) + rescue Fog::Compute::Vsphere::NotFound + nil + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/template.rb b/lib/fog/vsphere/models/compute/template.rb new file mode 100644 index 00000000..76140a35 --- /dev/null +++ b/lib/fog/vsphere/models/compute/template.rb @@ -0,0 +1,11 @@ +module Fog + module Compute + class Vsphere + class Template < Fog::Model + identity :id + attribute :name + attribute :uuid + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/templates.rb b/lib/fog/vsphere/models/compute/templates.rb new file mode 100644 index 00000000..bc76c851 --- /dev/null +++ b/lib/fog/vsphere/models/compute/templates.rb @@ -0,0 +1,20 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/template' + +module Fog + module Compute + class Vsphere + class Templates < Fog::Collection + model Fog::Compute::Vsphere::Template + + def all(filters = {}) + load service.list_templates(filters) + end + + def get(id) + new service.get_template(id) + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/volume.rb b/lib/fog/vsphere/models/compute/volume.rb new file mode 100644 index 00000000..38d9801d --- /dev/null +++ b/lib/fog/vsphere/models/compute/volume.rb @@ -0,0 +1,99 @@ +module Fog + module Compute + class Vsphere + class Volume < Fog::Model + DISK_SIZE_TO_GB = 1048576 + identity :id + + attribute :datastore + attribute :mode + attribute :size + attribute :thin + attribute :eager_zero + attribute :name + attribute :filename + attribute :size_gb + attribute :key + attribute :unit_number + attribute :server_id + + def initialize(attributes={} ) + # Assign server first to prevent race condition with persisted? + self.server_id = attributes.delete(:server_id) + + super defaults.merge(attributes) + end + + def size_gb + attributes[:size_gb] ||= attributes[:size].to_i / DISK_SIZE_TO_GB if attributes[:size] + end + + def size_gb= s + attributes[:size] = s.to_i * DISK_SIZE_TO_GB if s + end + + def to_s + name + end + + def destroy + requires :server_id, :key, :unit_number + + service.destroy_vm_volume(self) + true + end + + def save + raise Fog::Errors::Error.new('Resaving an existing object may create a duplicate') if persisted? + requires :server_id, :size, :datastore + + if unit_number.nil? + used_unit_numbers = server.volumes.map { |volume| volume.unit_number } + max_unit_number = used_unit_numbers.max + + if max_unit_number > server.volumes.size + # If the max ID exceeds the number of volumes, there must be a hole in the range. Find a hole and use it. + self.unit_number = max_unit_number.times.to_a.find { |i| used_unit_numbers.exclude?(i) } + else + self.unit_number = max_unit_number + 1 + end + else + if server.volumes.any? { |volume| volume.unit_number == self.unit_number && volume.id != self.id } + raise "A volume already exists with that unit_number, so we can't save the new volume" + end + end + + data = service.add_vm_volume(self) + + if data['task_state'] == 'success' + # We have to query vSphere to get the volume attributes since the task handle doesn't include that info. + created = server.volumes.all.find { |volume| volume.unit_number == self.unit_number } + + self.id = created.id + self.key = created.key + self.filename = created.filename + + true + else + false + end + end + + def server + requires :server_id + service.servers.get(server_id) + end + + private + + def defaults + { + :thin => true, + :name => "Hard disk", + :mode => "persistent" + } + end + end + end + end +end diff --git a/lib/fog/vsphere/models/compute/volumes.rb b/lib/fog/vsphere/models/compute/volumes.rb new file mode 100644 index 00000000..0aec5c7c --- /dev/null +++ b/lib/fog/vsphere/models/compute/volumes.rb @@ -0,0 +1,54 @@ +require 'fog/core/collection' +require 'fog/vsphere/models/compute/volume' + +module Fog + module Compute + class Vsphere + class Volumes < Fog::Collection + attribute :server_id + + model Fog::Compute::Vsphere::Volume + + def all(filters = {}) + requires :server_id + + case server + when Fog::Compute::Vsphere::Server + load service.list_vm_volumes(server.id) + when Fog::Compute::Vsphere::Template + load service.list_template_volumes(server.id) + else + raise 'volumes should have vm or template' + end + + self.each { |volume| volume.server_id = server.id } + self + end + + def get(id) + new service.get_volume(id) + end + + def new(attributes = {}) + if server_id + # Default to the root volume datastore if one is not configured. + datastore = ! attributes.key?(:datastore) && self.any? ? self.first.datastore : nil + + super({ :server_id => server_id, :datastore => datastore }.merge!(attributes)) + else + super + end + end + + def server + return nil if server_id.nil? + service.servers.get(server_id) + end + + def server=(new_server) + server_id = new_server.id + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/cloudinit_to_customspec.rb b/lib/fog/vsphere/requests/compute/cloudinit_to_customspec.rb new file mode 100644 index 00000000..b1e176ef --- /dev/null +++ b/lib/fog/vsphere/requests/compute/cloudinit_to_customspec.rb @@ -0,0 +1,65 @@ +module Fog + module Compute + class Vsphere + class Real + def cloudinit_to_customspec(user_data) + raise ArgumentError, "user_data can't be nil" if user_data.nil? + custom_spec = { 'customization_spec' => Hash.new } + user_data = YAML.load(user_data) + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Specification.html + # encryptionKey expects an array + # globalIPSettings expects a hash, REQUIRED + # identity expects an hash, REQUIRED + # nicSettingMap expects an array + # options expects an hash + + custom_spec['encryptionKey'] = user_data['encryptionKey'] if user_data.key?('encryptionKey') + custom_spec['globalIPSettings'] = user_data['globalIPSettings'] if user_data.key?('globalIPSettings') + custom_spec['identity'] = user_data['identity'] if user_data.key?('identity') + custom_spec['nicSettingMap'] = user_data['nicSettingMap'] if user_data.key?('nicSettingMap') + custom_spec['options'] = user_data['options'] if user_data.key?('options') + + # for backwards compatability + # hostname expects a string, REQUIRED + # netmask expects a string + # dns expects an array + # gateway expects an array + # domain expects a string, REQUIRED + # domainsuffixlist expects an array, REQUIRED + # timezone expects a string, for example Europe/Copenhagen, REQUIRED + custom_spec['hostname'] = user_data['hostname'] if user_data.key?('hostname') + custom_spec['ipsettings'] = { 'ip' => user_data['ip'] } if user_data.key?('ip') + custom_spec['ipsettings']['subnetMask'] = user_data['netmask'] if user_data.key?('netmask') + custom_spec['ipsettings']['dnsServerList'] = user_data['dns'] if user_data.key?('dns') + custom_spec['ipsettings']['gateway'] = user_data['gateway'] if user_data.key?('gateway') + custom_spec['domain'] = user_data['domain'] if user_data.key?('domain') + custom_spec['dnsSuffixList'] = user_data['domainsuffixlist'] if user_data.key?('domainsuffixlist') + custom_spec['time_zone'] = user_data['timezone'] if user_data.key?('timezone') + custom_spec + end + end + + class Mock + def cloudinit_to_customspec(user_data) + raise ArgumentError, "user_data can't be nil" if user_data.nil? + custom_spec = { 'customization_spec' => Hash.new } + user_data = YAML.load(user_data) + custom_spec['encryptionKey'] = user_data['encryptionKey'] if user_data.key?('encryptionKey') + custom_spec['globalIPSettings'] = user_data['globalIPSettings'] if user_data.key?('globalIPSettings') + custom_spec['identity'] = user_data['identity'] if user_data.key?('identity') + custom_spec['nicSettingMap'] = user_data['nicSettingMap'] if user_data.key?('nicSettingMap') + custom_spec['options'] = user_data['options'] if user_data.key?('options') + custom_spec['hostname'] = user_data['hostname'] if user_data.key?('hostname') + custom_spec['ipsettings'] = { 'ip' => user_data['ip'] } if user_data.key?('ip') + custom_spec['ipsettings']['subnetMask'] = user_data['netmask'] if user_data.key?('netmask') + custom_spec['ipsettings']['dnsServerList'] = user_data['dns'] if user_data.key?('dns') + custom_spec['ipsettings']['gateway'] = user_data['gateway'] if user_data.key?('gateway') + custom_spec['domain'] = user_data['domain'] if user_data.key?('domain') + custom_spec['dnsSuffixList'] = user_data['domainsuffixlist'] if user_data.key?('domainsuffixlist') + custom_spec['time_zone'] = user_data['timezone'] if user_data.key?('timezone') + custom_spec + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/create_folder.rb b/lib/fog/vsphere/requests/compute/create_folder.rb new file mode 100644 index 00000000..4baa5644 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/create_folder.rb @@ -0,0 +1,22 @@ +module Fog + module Compute + class Vsphere + class Real + def create_folder(datacenter, path, name) + #Path cannot be nil but it can be an empty string + raise ArgumentError, "Path cannot be nil" if path.nil? + + parent_folder = get_raw_vmfolder(path, datacenter) + begin + new_folder = parent_folder.CreateFolder(:name => name) + # output is cleaned up to return the new path + # new path will be path/name, example: "Production/Pool1" + new_folder.path.reject { |a| a.first.class == "Folder" }.map { |a| a.first.name }.join("/").sub(/^\/?Datacenters\/#{datacenter}\/vm\/?/, '') + rescue => e + raise e, "failed to create folder: #{e}" + end + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/create_vm.rb b/lib/fog/vsphere/requests/compute/create_vm.rb new file mode 100644 index 00000000..84eb989b --- /dev/null +++ b/lib/fog/vsphere/requests/compute/create_vm.rb @@ -0,0 +1,169 @@ +module Fog + module Compute + class Vsphere + class Real + def create_vm attributes = { } + # build up vm configuration + + vm_cfg = { + :name => attributes[:name], + :guestId => attributes[:guest_id], + :version => attributes[:hardware_version], + :files => { :vmPathName => vm_path_name(attributes) }, + :numCPUs => attributes[:cpus], + :numCoresPerSocket => attributes[:corespersocket], + :memoryMB => attributes[:memory_mb], + :deviceChange => device_change(attributes), + :extraConfig => extra_config(attributes), + } + vm_cfg[:cpuHotAddEnabled] = attributes[:cpuHotAddEnabled] if attributes.key?(:cpuHotAddEnabled) + vm_cfg[:memoryHotAddEnabled] = attributes[:memoryHotAddEnabled] if attributes.key?(:memoryHotAddEnabled) + vm_cfg[:firmware] = attributes[:firmware] if attributes.key?(:firmware) + resource_pool = if attributes[:resource_pool] + get_raw_resource_pool(attributes[:resource_pool], attributes[:cluster], attributes[:datacenter]) + else + get_raw_cluster(attributes[:cluster], attributes[:datacenter]).resourcePool + end + vmFolder = get_raw_vmfolder(attributes[:path], attributes[:datacenter]) + vm = vmFolder.CreateVM_Task(:config => vm_cfg, :pool => resource_pool).wait_for_completion + vm.config.instanceUuid + rescue => e + raise e, "failed to create vm: #{e}" + end + + private + + # this methods defines where the vm config files would be located, + # by default we prefer to keep it at the same place the (first) vmdk is located + def vm_path_name attributes + datastore = attributes[:volumes].first.datastore unless attributes[:volumes].empty? + datastore ||= 'datastore1' + "[#{datastore}]" + end + + def device_change attributes + devices = [] + if (nics = attributes[:interfaces]) + devices << nics.map { |nic| create_interface(nic, nics.index(nic), :add, attributes) } + end + + if (disks = attributes[:volumes]) + devices << create_controller(attributes[:scsi_controller]||attributes["scsi_controller"]||{}) + devices << disks.map { |disk| create_disk(disk, disks.index(disk)) } + end + devices.flatten + end + + def create_nic_backing nic, attributes + raw_network = get_raw_network(nic.network, attributes[:datacenter], if nic.virtualswitch then nic.virtualswitch end) + + if raw_network.kind_of? RbVmomi::VIM::DistributedVirtualPortgroup + RbVmomi::VIM.VirtualEthernetCardDistributedVirtualPortBackingInfo( + :port => RbVmomi::VIM.DistributedVirtualSwitchPortConnection( + :portgroupKey => raw_network.key, + :switchUuid => raw_network.config.distributedVirtualSwitch.uuid + ) + ) + else + RbVmomi::VIM.VirtualEthernetCardNetworkBackingInfo(:deviceName => nic.network) + end + end + + def create_interface nic, index = 0, operation = :add, attributes = {} + { + :operation => operation, + :device => nic.type.new( + :key => index, + :deviceInfo => + { + :label => nic.name, + :summary => nic.summary, + }, + :backing => create_nic_backing(nic, attributes), + :addressType => 'generated') + } + end + + def create_controller options=nil + options=if options + controller_default_options.merge(Hash[options.map{|k,v| [k.to_sym,v] }]) + else + controller_default_options + end + controller_class=if options[:type].is_a? String then + Fog::Vsphere.class_from_string options[:type], "RbVmomi::VIM" + else + options[:type] + end + { + :operation => options[:operation], + :device => controller_class.new({ + :key => options[:key], + :busNumber => options[:bus_id], + :sharedBus => controller_get_shared_from_options(options), + }) + } + end + + def controller_default_options + {:operation => "add", :type => RbVmomi::VIM.VirtualLsiLogicController.class, :key => 1000, :bus_id => 0, :shared => false } + end + + def controller_get_shared_from_options options + if (options.key? :shared and options[:shared]==false) or not options.key? :shared then + :noSharing + elsif options[:shared]==true then + :virtualSharing + elsif options[:shared].is_a? String + options[:shared] + else + :noSharing + end + end + + def create_disk disk, index = 0, operation = :add, controller_key = 1000 + if (index > 6) then + _index = index + 1 + else + _index = index + end + payload = { + :operation => operation, + :fileOperation => operation == :add ? :create : :destroy, + :device => RbVmomi::VIM.VirtualDisk( + :key => disk.key || _index, + :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo( + :fileName => "[#{disk.datastore}]", + :diskMode => disk.mode.to_sym, + :thinProvisioned => disk.thin + ), + :controllerKey => controller_key, + :unitNumber => _index, + :capacityInKB => disk.size + ) + } + + if operation == :add && disk.thin == 'false' && disk.eager_zero == 'true' + payload[:device][:backing][:eagerlyScrub] = disk.eager_zero + end + + payload + end + + def extra_config attributes + [ + { + :key => 'bios.bootOrder', + :value => 'ethernet0' + } + ] + end + end + + class Mock + def create_vm attributes = { } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/current_time.rb b/lib/fog/vsphere/requests/compute/current_time.rb new file mode 100644 index 00000000..78bd4fb1 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/current_time.rb @@ -0,0 +1,18 @@ +module Fog + module Compute + class Vsphere + class Real + def current_time + current_time = @connection.serviceInstance.CurrentTime + { 'current_time' => current_time } + end + end + + class Mock + def current_time + { 'current_time' => Time.now.utc } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_cluster.rb b/lib/fog/vsphere/requests/compute/get_cluster.rb new file mode 100644 index 00000000..61050959 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_cluster.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class Vsphere + class Real + def get_cluster(name, datacenter_name) + cluster = get_raw_cluster(name, datacenter_name) + raise(Fog::Compute::Vsphere::NotFound) unless cluster + cluster_attributes(cluster, datacenter_name) + end + + protected + + def get_raw_cluster(name, datacenter_name) + dc = find_raw_datacenter(datacenter_name) + dc.find_compute_resource(name) + end + end + + class Mock + def get_cluster(id) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_compute_resource.rb b/lib/fog/vsphere/requests/compute/get_compute_resource.rb new file mode 100644 index 00000000..be3119df --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_compute_resource.rb @@ -0,0 +1,41 @@ +module Fog + module Compute + class Vsphere + class Real + def get_compute_resource(name, datacenter_name) + compute_resource = get_raw_compute_resource(name, datacenter_name) + raise(Fog::Compute::Vsphere::NotFound) unless compute_resource + compute_resource_attributes(compute_resource, datacenter_name) + end + + protected + + def get_raw_compute_resource(name, datacenter_name) + find_raw_datacenter(datacenter_name).find_compute_resource(name) + end + end + + class Mock + def get_compute_resource(name, datacenter_name) + { + :id=>"domain-s7", + :name=>"fake-host", + :totalCpu=>33504, + :totalMemory=>154604142592, + :numCpuCores=>12, + :numCpuThreads=>24, + :effectiveCpu=>32247, + :effectiveMemory=>135733, + :numHosts=>1, + :numEffectiveHosts=>1, + :overallStatus=>"gray", + :overallCpuUsage=>15682, + :overallMemoryUsage=>132755, + :effective=>true, + :isSingleHost=>true + } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_datacenter.rb b/lib/fog/vsphere/requests/compute/get_datacenter.rb new file mode 100644 index 00000000..8c81f509 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_datacenter.rb @@ -0,0 +1,31 @@ +module Fog + module Compute + class Vsphere + class Real + def get_datacenter name + dc = find_raw_datacenter(name) + raise(Fog::Compute::Vsphere::NotFound) unless dc + {:name => dc.name, :status => dc.overallStatus, :path => raw_getpathmo(dc) } + end + + protected + + def find_raw_datacenter name + raw_datacenters.find {|d| d.name == name} || get_raw_datacenter(name) + end + + def get_raw_datacenter name + @connection.serviceInstance.find_datacenter(name) + end + end + + class Mock + def get_datacenter name + dc = self.data[:datacenters][name] + raise(Fog::Compute::Vsphere::NotFound) unless dc + dc + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_datastore.rb b/lib/fog/vsphere/requests/compute/get_datastore.rb new file mode 100644 index 00000000..8d8fe600 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_datastore.rb @@ -0,0 +1,30 @@ +module Fog + module Compute + class Vsphere + class Real + def get_datastore(name, datacenter_name) + datastore = get_raw_datastore(name, datacenter_name) + raise(Fog::Compute::Vsphere::NotFound) unless datastore + datastore_attributes(datastore, datacenter_name) + end + + protected + + def get_raw_datastore(name, datacenter_name) + dc = find_raw_datacenter(datacenter_name) + + @connection.serviceContent.viewManager.CreateContainerView({ + :container => dc.datastoreFolder, + :type => ["Datastore"], + :recursive => true + }).view.select{|ds| ds.name == name}.first + end + end + + class Mock + def get_datastore(name, datacenter_name) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_folder.rb b/lib/fog/vsphere/requests/compute/get_folder.rb new file mode 100644 index 00000000..7e1c57c0 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_folder.rb @@ -0,0 +1,74 @@ +module Fog + module Compute + class Vsphere + class Real + def get_folder(path, datacenter_name, type = nil) + type ||= 'vm' + + # Cycle through all types of folders. + case type + when 'vm', :vm + # if you're a vm then grab the VM. + folder = get_raw_vmfolder(path, datacenter_name) + raise(Fog::Compute::Vsphere::NotFound) unless folder + folder_attributes(folder, datacenter_name) + when 'network', :network + raise "not implemented" + when 'datastore', :datastore + raise "not implemented" + else + raise ArgumentError, "#{type} is unknown" + end + end + + protected + + def get_raw_vmfolder(path, datacenter_name) + # The required path syntax - 'topfolder/subfolder + + # Clean up path to be relative since we're providing datacenter name + dc = find_raw_datacenter(datacenter_name) + dc_root_folder = dc.vmFolder + # Filter the root path for this datacenter not to be used." + dc_root_folder_path=dc_root_folder.path.map { | id, name | name }.join("/") + paths = path.sub(/^\/?#{Regexp.quote(dc_root_folder_path)}\/?/, '').split('/') + + return dc_root_folder if paths.empty? + # Walk the tree resetting the folder pointer as we go + paths.reduce(dc_root_folder) do |last_returned_folder, sub_folder| + # JJM VIM::Folder#find appears to be quite efficient as it uses the + # searchIndex It certainly appears to be faster than + # VIM::Folder#inventory since that returns _all_ managed objects of + # a certain type _and_ their properties. + sub = last_returned_folder.find(sub_folder, RbVmomi::VIM::Folder) + raise ArgumentError, "Could not descend into #{sub_folder}. Please check your path. #{path}" unless sub + sub + end + end + + def folder_attributes(folder, datacenter_name) + { + :id => managed_obj_id(folder), + :name => folder.name, + :parent => folder.parent.name, + :datacenter => datacenter_name, + :type => folder_type(folder), + :path => "/"+folder.path.map(&:last).join('/'), + } + end + + def folder_type(folder) + types = folder.childType + return :vm if types.include?('VirtualMachine') + return :network if types.include?('Network') + return :datastore if types.include?('Datastore') + end + end + + class Mock + def get_folder(path, filters = { }) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_interface_type.rb b/lib/fog/vsphere/requests/compute/get_interface_type.rb new file mode 100644 index 00000000..27d3c866 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_interface_type.rb @@ -0,0 +1,15 @@ +module Fog + module Compute + class Vsphere + class Real + def get_interface_type(id, servertype, datacenter, filter={}) + interfacetype=list_interface_types(filters={:id => id, + :datacenter => datacenter, + :servertype => servertype.id }).first + raise(Fog::Compute::Vsphere::NotFound) unless interfacetype + interfacetype + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_network.rb b/lib/fog/vsphere/requests/compute/get_network.rb new file mode 100644 index 00000000..a1cb7532 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_network.rb @@ -0,0 +1,59 @@ +module Fog + module Compute + class Vsphere + class Real + def get_network(name, datacenter_name) + network = get_raw_network(name, datacenter_name) + raise(Fog::Compute::Vsphere::NotFound) unless network + network_attributes(network, datacenter_name) + end + + protected + + def get_raw_network(name, datacenter_name, distributedswitch=nil) + finder = choose_finder(name, distributedswitch) + networks = get_all_raw_networks(datacenter_name) + networks.find { |n| finder.call(n) } + end + end + + module Shared + + protected + + def get_all_raw_networks(datacenter_name) + dc = find_raw_datacenter(datacenter_name) + @connection.serviceContent.viewManager. + CreateContainerView({ + :container => dc.networkFolder, + :type => ["Network"], + :recursive => true + }).view + end + + def choose_finder(name, distributedswitch) + case distributedswitch + when String + # only the one will do + Proc.new { |n| (n.name == name) && + (n.class.to_s == "DistributedVirtualPortgroup") && + (n.config.distributedVirtualSwitch.name == distributedswitch) + } + when :dvs + # the first distributed virtual switch will do - selected by network - gives control to vsphere + Proc.new { |n| (n.name == name) && (n.class.to_s == "DistributedVirtualPortgroup") } + else + # the first matching network will do, seems like the non-distributed networks come first + Proc.new { |n| (n.name == name) } + end + end + + end + + class Mock + def get_network(id) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_resource_pool.rb b/lib/fog/vsphere/requests/compute/get_resource_pool.rb new file mode 100644 index 00000000..483a2b85 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_resource_pool.rb @@ -0,0 +1,26 @@ +module Fog + module Compute + class Vsphere + class Real + def get_resource_pool(name, cluster_name, datacenter_name) + resource_pool = get_raw_resource_pool(name, cluster_name, datacenter_name) + raise(Fog::Compute::Vsphere::NotFound) unless resource_pool + resource_pool_attributes(resource_pool, cluster_name, datacenter_name) + end + + protected + + def get_raw_resource_pool(name, cluster_name, datacenter_name) + dc = find_raw_datacenter(datacenter_name) + cluster = dc.find_compute_resource(cluster_name) + name.nil? ? cluster.resourcePool : cluster.resourcePool.traverse(name) + end + end + + class Mock + def get_resource_pool(name, cluster_name, datacenter_name) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_server_type.rb b/lib/fog/vsphere/requests/compute/get_server_type.rb new file mode 100644 index 00000000..8335baa2 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_server_type.rb @@ -0,0 +1,32 @@ +module Fog + module Compute + class Vsphere + class Real + def get_server_type(id, datacenter, filter={}) + server_type=get_raw_server_type(id, datacenter) + raise(Fog::Compute::Vsphere::NotFound) unless server_type + server_type_attributes(server_type, datacenter) + end + + protected + + def get_raw_server_type(id, datacenter, filter={}) + types=raw_server_types(datacenter) + raise(Fog::Compute::Vsphere::NotFound) unless types + types=types.select{ | servertype | servertype.id == id }.first + raise(Fog::Compute::Vsphere::NotFound) unless types + types + end + end + class Mock + def get_server_type(id) + {:id=>"rhel6Guest", + :name=>"rhel6Guest", + :family=>"linuxGuest", + :fullname=>"Red Hat Enterprise Linux 6 (32-Bit)", + :datacenter=>"Solutions"} + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_template.rb b/lib/fog/vsphere/requests/compute/get_template.rb new file mode 100644 index 00000000..94565e8b --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_template.rb @@ -0,0 +1,16 @@ +module Fog + module Compute + class Vsphere + class Real + def get_template(id, datacenter_name = nil) + convert_vm_mob_ref_to_attr_hash(get_vm_ref(id, datacenter_name)) + end + end + + class Mock + def get_template(id, datacenter_name = nil) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_virtual_machine.rb b/lib/fog/vsphere/requests/compute/get_virtual_machine.rb new file mode 100644 index 00000000..ded4a7b6 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_virtual_machine.rb @@ -0,0 +1,57 @@ +module Fog + module Compute + class Vsphere + class Real + def get_virtual_machine(id, datacenter_name = nil) + # The larger the VM list the longer it will take if not searching based on UUID. + convert_vm_mob_ref_to_attr_hash(get_vm_ref(id, datacenter_name)) + end + + protected + + def get_vm_ref(id, dc = nil) + raw_datacenter = find_raw_datacenter(dc) if dc + vm = case is_uuid?(id) + # UUID based + when true + params = {:uuid => id, :vmSearch => true, :instanceUuid => true} + params[:datacenter] = raw_datacenter if dc + @connection.searchIndex.FindByUuid(params) + else + # try to find based on VM name + if dc + get_vm_by_name(id, dc) + else + raw_datacenters.map { |d| get_vm_by_name(id, d["name"])}.compact.first + end + end + vm ? vm : raise(Fog::Compute::Vsphere::NotFound, "#{id} was not found") + end + + def get_vm_by_name(name, dc) + vms = raw_list_all_virtual_machines(dc) + + if name.include?('/') + folder = File.dirname(name) + basename = File.basename(name) + vms.keep_if { |v| v["name"] == basename && v.parent.pretty_path.include?(folder) }.first + else + vms.keep_if { |v| v["name"] == name }.first + end + end + end + + class Mock + def get_virtual_machine(id, datacenter_name = nil) + if is_uuid?(id) + vm = list_virtual_machines({ 'instance_uuid' => id, 'datacenter' => datacenter_name }).first + else + # try to find based on VM name. May need to handle the path of the VM + vm = list_virtual_machines({ 'name' => id, 'datacenter' => datacenter_name }).first + end + vm ? vm : raise(Fog::Compute::Vsphere::NotFound, "#{id} was not found") + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/get_vm_first_scsi_controller.rb b/lib/fog/vsphere/requests/compute/get_vm_first_scsi_controller.rb new file mode 100644 index 00000000..0b2d80e3 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/get_vm_first_scsi_controller.rb @@ -0,0 +1,26 @@ + +module Fog + module Compute + class Vsphere + class Real + def get_vm_first_scsi_controller(vm_id) + Fog::Compute::Vsphere::SCSIController.new(get_vm_first_scsi_controller_raw(vm_id)) + end + + def get_vm_first_scsi_controller_raw(vm_id) + ctrl=get_vm_ref(vm_id).config.hardware.device.grep(RbVmomi::VIM::VirtualSCSIController).select{ | ctrl | ctrl.key == 1000 }.first + { + :type => ctrl.class.to_s, + :shared_bus => ctrl.sharedBus.to_s, + :unit_number => ctrl.scsiCtlrUnitNumber, + :key => ctrl.key, + } + end + end + class Mock + def get_vm_first_scsi_controller(vm_id) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_clusters.rb b/lib/fog/vsphere/requests/compute/list_clusters.rb new file mode 100644 index 00000000..3add8262 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_clusters.rb @@ -0,0 +1,72 @@ +module Fog + module Compute + class Vsphere + class Real + def list_clusters(filters = { }) + datacenter_name = filters[:datacenter] + + raw_clusters(datacenter_name).map do |cluster| + cluster_attributes(cluster, datacenter_name) + end + end + + def raw_clusters(datacenter) + folder ||= find_raw_datacenter(datacenter).hostFolder + @raw_clusters = get_raw_clusters_from_folder(folder) + end + + protected + + def get_raw_clusters_from_folder(folder) + folder.childEntity.map do |child_entity| + if child_entity.is_a? RbVmomi::VIM::ComputeResource + child_entity + elsif child_entity.is_a? RbVmomi::VIM::Folder + get_raw_clusters_from_folder(child_entity) + end + end.flatten + end + + def cluster_attributes cluster, datacenter_name + { + :id => managed_obj_id(cluster), + :name => cluster.name, + :full_path => cluster_path(cluster, datacenter_name), + :num_host => cluster.summary.numHosts, + :num_cpu_cores => cluster.summary.numCpuCores, + :overall_status => cluster.summary.overallStatus, + :datacenter => datacenter_name || parent_attribute(cluster.path, :datacenter)[1], + } + end + + def cluster_path(cluster, datacenter_name) + datacenter = find_raw_datacenter(datacenter_name) + cluster.pretty_path.gsub(/(#{datacenter_name}|#{datacenter.hostFolder.name})\//,'') + end + end + + class Mock + def list_clusters(filters = { }) + raw_clusters.map do |cluster| + cluster + end + end + + def raw_clusters + folder = self.data[:clusters] + @raw_clusters = get_raw_clusters_from_folder(folder) + end + + def get_raw_clusters_from_folder(folder) + folder.map do |child| + if child[:klass] == "RbVmomi::VIM::ComputeResource" + child + elsif child[:klass] == "RbVmomi::VIM::Folder" + get_raw_clusters_from_folder(child[:clusters]) + end + end.flatten + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_compute_resources.rb b/lib/fog/vsphere/requests/compute/list_compute_resources.rb new file mode 100644 index 00000000..572ebc0a --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_compute_resources.rb @@ -0,0 +1,92 @@ +module Fog + module Compute + class Vsphere + class Real + def list_compute_resources(filters = { }) + datacenter_name = filters[:datacenter] + # default to show all compute_resources + only_active = filters[:effective] || false + compute_resources = raw_compute_resources datacenter_name + + compute_resources.map do |compute_resource| + summary = compute_resource.summary + next if only_active and summary.numEffectiveHosts == 0 + compute_resource_attributes(compute_resource, datacenter_name) + end.compact + end + + def raw_compute_resources(datacenter_name) + find_raw_datacenter(datacenter_name).find_compute_resource('').children + end + + protected + + def compute_resource_attributes compute_resource, datacenter + overall_usage = compute_resource.host.inject({:overallCpuUsage=>0, :overallMemoryUsage=>0}) do |sum, host| + { + :overallCpuUsage => sum[:overallCpuUsage]+(host.summary.quickStats.overallCpuUsage || 0), + :overallMemoryUsage=> sum[:overallMemoryUsage]+(host.summary.quickStats.overallMemoryUsage || 0) + } + end + { + :id => managed_obj_id(compute_resource), + :name => compute_resource.name, + :totalCpu => compute_resource.summary.totalCpu, + :totalMemory => compute_resource.summary.totalMemory, + :numCpuCores => compute_resource.summary.numCpuCores, + :numCpuThreads => compute_resource.summary.numCpuThreads, + :effectiveCpu => compute_resource.summary.effectiveCpu, + :effectiveMemory => compute_resource.summary.effectiveMemory, + :numHosts => compute_resource.summary.numHosts, + :numEffectiveHosts => compute_resource.summary.numEffectiveHosts, + :overallStatus => compute_resource.summary.overallStatus, + :overallCpuUsage => overall_usage[:overallCpuUsage], + :overallMemoryUsage => overall_usage[:overallMemoryUsage], + :effective => compute_resource.summary.numEffectiveHosts > 0, + :isSingleHost => compute_resource.summary.numHosts == 1 + } + end + + end + class Mock + def list_compute_resources(filters = { }) + [ + { + :id=>"domain-s7", + :name=>"fake-host", + :totalCpu=>33504, + :totalMemory=>154604142592, + :numCpuCores=>12, + :numCpuThreads=>24, + :effectiveCpu=>32247, + :effectiveMemory=>135733, + :numHosts=>1, + :numEffectiveHosts=>1, + :overallStatus=>"gray", + :overallCpuUsage=>15682, + :overallMemoryUsage=>132755, + :effective=>true, + :isSingleHost=>true + }, { + :id=>"domain-s74", + :name=>"fake-cluster", + :totalCpu=>41484, + :totalMemory=>51525996544, + :numCpuCores=>12, + :numCpuThreads=>24, + :effectiveCpu=>37796, + :effectiveMemory=>45115, + :numHosts=>2, + :numEffectiveHosts=>2, + :overallStatus=>"gray", + :overallCpuUsage=>584, + :overallMemoryUsage=>26422, + :effective=>true, + :isSingleHost=>false + } + ] + end + end + end + end +end \ No newline at end of file diff --git a/lib/fog/vsphere/requests/compute/list_customfields.rb b/lib/fog/vsphere/requests/compute/list_customfields.rb new file mode 100644 index 00000000..1ea35e47 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_customfields.rb @@ -0,0 +1,21 @@ +module Fog + module Compute + class Vsphere + class Real + def list_customfields() + @connection.serviceContent.customFieldsManager.field.map do |customfield| + { + :key => customfield.key.to_i, + :name => customfield.name, + :type => customfield.type + } + end + end + end + class Mock + def list_vm_customfields() + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_datacenters.rb b/lib/fog/vsphere/requests/compute/list_datacenters.rb new file mode 100644 index 00000000..8ec3a741 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_datacenters.rb @@ -0,0 +1,53 @@ +module Fog + module Compute + class Vsphere + class Real + def list_datacenters filters = {} + raw_datacenters.map do |dc| + { + :id => managed_obj_id(dc), + :name => dc.name, + :path => raw_getpathmo(dc), + :status => dc.overallStatus + } + end + end + + protected + + def raw_getpathmo mo + if mo.parent == nil or mo.parent.name == @connection.rootFolder.name then + [ mo.name ] + else + [ raw_getpathmo(mo.parent), mo.name ].flatten + end + end + + def raw_datacenters folder=nil + folder ||= @connection.rootFolder + @raw_datacenters ||= get_raw_datacenters_from_folder folder + end + + def get_raw_datacenters_from_folder folder=nil + folder.childEntity.map do | childE | + if childE.is_a? RbVmomi::VIM::Datacenter + childE + elsif childE.is_a? RbVmomi::VIM::Folder + get_raw_datacenters_from_folder childE + end + end.flatten + end + + def find_datacenters name=nil + name ? [find_raw_datacenter(name)] : raw_datacenters + end + end + + class Mock + def list_datacenters filters = {} + [ {:name => "Solutions", :status => "grey"}, {:name => "Solutions2", :status => "green" }] + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_datastores.rb b/lib/fog/vsphere/requests/compute/list_datastores.rb new file mode 100644 index 00000000..037ffde3 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_datastores.rb @@ -0,0 +1,40 @@ +module Fog + module Compute + class Vsphere + class Real + def list_datastores(filters = { }) + datacenter_name = filters[:datacenter] + # default to show all datastores + only_active = filters[:accessible] || false + raw_datastores(datacenter_name).map do |datastore| + next if only_active and !datastore.summary.accessible + datastore_attributes(datastore, datacenter_name) + end.compact + end + + def raw_datastores(datacenter_name) + find_raw_datacenter(datacenter_name).datastore + end + protected + + def datastore_attributes datastore, datacenter + { + :id => managed_obj_id(datastore), + :name => datastore.name, + :accessible => datastore.summary.accessible, + :type => datastore.summary.type, + :freespace => datastore.summary.freeSpace, + :capacity => datastore.summary.capacity, + :uncommitted => datastore.summary.uncommitted, + :datacenter => datacenter, + } + end + end + class Mock + def list_datastores(datacenter_name) + [] + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_folders.rb b/lib/fog/vsphere/requests/compute/list_folders.rb new file mode 100644 index 00000000..3f340311 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_folders.rb @@ -0,0 +1,44 @@ +module Fog + module Compute + class Vsphere + class Real + # Grabs all sub folders within a given path folder. + # + # ==== Parameters + # * filters<~Hash>: + # * :datacenter<~String> - *REQUIRED* Your datacenter where you're + # looking for folders. Example: 'my-datacenter-name' (passed if you + # are using the models/collections) + # eg: vspconn.datacenters.first.vm_folders('mypath') + # * :path<~String> - Your path where you're looking for + # more folders, if return = none you will get an error. If you don't + # define it will look in the main datacenter folder for any folders + # in that datacenter. + # + # Example Usage Testing Only: + # vspconn = Fog::Compute[:vsphere] + # mydc = vspconn.datacenters.first + # folders = mydc.vm_folders + # + def list_folders(filters = { }) + path = filters[:path] || filters['path'] || '' + datacenter_name = filters[:datacenter] + get_raw_vmfolders(path, datacenter_name).map do |folder| + folder_attributes(folder, datacenter_name) + end + end + + protected + + def get_raw_vmfolders(path, datacenter_name) + folder = get_raw_vmfolder(path, datacenter_name) + child_folders(folder).flatten.compact + end + + def child_folders folder + [folder, folder.childEntity.grep(RbVmomi::VIM::Folder).map(&method(:child_folders)).flatten] + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_interface_types.rb b/lib/fog/vsphere/requests/compute/list_interface_types.rb new file mode 100644 index 00000000..561fc2fb --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_interface_types.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class Vsphere + class Real + def list_interface_types(filters={}) + datacenter_name = filters[:datacenter] + servertype_name = filters[:servertype] + get_raw_server_type(servertype_name, datacenter_name)[:supportedEthernetCard].map do | nictype | + next if filters.key?(:id) and filters[:id] != nictype + interface_type_attributes(nictype, servertype_name, datacenter_name) + end.compact + end + + def interface_type_attributes(nic, servertype, datacenter) + { + :id => nic, + :name => nic, + :datacenter => datacenter, + :servertype => servertype + } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_networks.rb b/lib/fog/vsphere/requests/compute/list_networks.rb new file mode 100644 index 00000000..b2aee8ac --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_networks.rb @@ -0,0 +1,38 @@ +module Fog + module Compute + class Vsphere + class Real + def list_networks(filters = { }) + datacenter_name = filters[:datacenter] + # default to show all networks + only_active = filters[:accessible] || false + raw_networks(datacenter_name).map do |network| + next if only_active and !network.summary.accessible + network_attributes(network, datacenter_name) + end.compact + end + + def raw_networks(datacenter_name) + find_raw_datacenter(datacenter_name).network + end + + protected + + def network_attributes network, datacenter + { + :id => managed_obj_id(network), + :name => network.name, + :accessible => network.summary.accessible, + :datacenter => datacenter, + :virtualswitch => network.class.name == "DistributedVirtualPortgroup" ? network.config.distributedVirtualSwitch.name : nil + } + end + end + class Mock + def list_networks(datacenter_name) + [] + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_resource_pools.rb b/lib/fog/vsphere/requests/compute/list_resource_pools.rb new file mode 100644 index 00000000..aedc4f46 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_resource_pools.rb @@ -0,0 +1,38 @@ +module Fog + module Compute + class Vsphere + class Real + def list_resource_pools(filters = { }) + datacenter_name = filters[:datacenter] + cluster_name = filters[:cluster] + cluster = get_raw_cluster(cluster_name, datacenter_name) + list_raw_resource_pools(cluster).map do |resource_pool| + resource_pool_attributes(resource_pool, cluster_name, datacenter_name) + end + end + + protected + + # root ResourcePool + Children if they exists + def list_raw_resource_pools(cluster) + [cluster.resourcePool, cluster.resourcePool.resourcePool].flatten + end + + def resource_pool_attributes resource_pool, cluster, datacenter + { + :id => managed_obj_id(resource_pool), + :name => resource_pool.name, + :configured_memory_mb => resource_pool.summary.configuredMemoryMB, + :overall_status => resource_pool.overallStatus, + :cluster => cluster, + :datacenter => datacenter + } + end + end + class Mock + def list_resource_pools(filters = { }) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_server_types.rb b/lib/fog/vsphere/requests/compute/list_server_types.rb new file mode 100644 index 00000000..d2215770 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_server_types.rb @@ -0,0 +1,54 @@ +module Fog + module Compute + class Vsphere + class Real + def list_server_types(filters={}) + datacenter_name = filters[:datacenter] + servertypes=raw_server_types(datacenter_name) + if servertypes + servertypes.map do | servertype | + server_type_attributes(servertype, datacenter_name) + end.compact + else + nil + end + #select{ | guestdesc | guestdesc.select{ | k, v | filter.has_key?(k) and filter[k] == v }==filter } + end + + def raw_server_types(datacenter_name, filter={}) + datacenter=find_raw_datacenter(datacenter_name) + environmentBrowser=datacenter.hostFolder.childEntity.grep(RbVmomi::VIM::ComputeResource).first.environmentBrowser + if environmentBrowser + environmentBrowser.QueryConfigOption[:guestOSDescriptor] + end + end + + protected + + def server_type_attributes(servertype, datacenter) + { + :id => servertype.id, + :name => servertype.id, + :family => servertype.family, + :fullname => servertype.fullName, + :datacenter => datacenter, + } + end + end + class Mock + def list_server_types(datacenter_name) + [{:id=>"rhel6Guest", + :name=>"rhel6Guest", + :family=>"linuxGuest", + :fullname=>"Red Hat Enterprise Linux 6 (32-Bit)", + :datacenter=>"Solutions"}, + {:id=>"rhel5_64Guest", + :name=>"rhel5_64Guest", + :family=>"linuxGuest", + :fullname=>"Red Hat Enterprise Linux 5 (64-Bit)", + :datacenter=>"Solutions"}] + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_templates.rb b/lib/fog/vsphere/requests/compute/list_templates.rb new file mode 100644 index 00000000..52011aa6 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_templates.rb @@ -0,0 +1,48 @@ +module Fog + module Compute + class Vsphere + class Real + def list_templates(options = { }) + options[:folder] ||= options['folder'] + if options[:folder] then + list_all_templates_in_folder(options[:folder], options[:datacenter]) + else + list_all_templates(options) + end + end + + private + + def list_all_templates_in_folder(path, datacenter_name) + folder = get_raw_vmfolder(path, datacenter_name) + + vms = folder.children.grep(RbVmomi::VIM::VirtualMachine) + # remove all virtual machines that are not template + vms.delete_if { |v| v.config.nil? or not v.config.template } + + vms.map(&method(:convert_vm_mob_ref_to_attr_hash)) + end + + def list_all_templates(options = {}) + datacenters = find_datacenters(options[:datacenter]) + + vms = datacenters.map do |dc| + @connection.serviceContent.viewManager.CreateContainerView({ + :container => dc.vmFolder, + :type => ["VirtualMachine"], + :recursive => true + }).view + end.flatten + # remove all virtual machines that are not templates + vms.delete_if { |v| v.config.nil? or not v.config.template } + + vms.map(&method(:convert_vm_mob_ref_to_attr_hash)) + end + end + class Mock + def list_templates(filters = { }) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_virtual_machines.rb b/lib/fog/vsphere/requests/compute/list_virtual_machines.rb new file mode 100644 index 00000000..3510f095 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_virtual_machines.rb @@ -0,0 +1,80 @@ +module Fog + module Compute + class Vsphere + class Real + def list_virtual_machines(options = { }) + # Listing all VM's can be quite slow and expensive. Try and optimize + # based on the available options we have. These conditions are in + # ascending order of time to complete for large deployments. + + options[:folder] ||= options['folder'] + if options['instance_uuid'] then + [get_virtual_machine(options['instance_uuid'])] + elsif options[:folder] && options[:datacenter] then + list_all_virtual_machines_in_folder(options[:folder], options[:datacenter]) + else + list_all_virtual_machines(options) + end + end + + + private + + def list_all_virtual_machines_in_folder(path, datacenter_name) + folder = get_raw_vmfolder(path, datacenter_name) + + vms = folder.children.grep(RbVmomi::VIM::VirtualMachine) + # remove all template based virtual machines + vms.delete_if { |v| v.config.nil? or v.config.template } + vms.map(&method(:convert_vm_mob_ref_to_attr_hash)) + end + + def list_all_virtual_machines(options = { }) + raw_vms = raw_list_all_virtual_machines(options[:datacenter]) + vms = convert_vm_view_to_attr_hash(raw_vms) + + # remove all template based virtual machines + vms.delete_if { |v| v['template'] } + vms + end + + def raw_list_all_virtual_machines(datacenter_name = nil) + ## Moved this to its own function since trying to get a list of all virtual machines + ## to parse for a find function took way too long. The raw list returned will make it + ## much faster to interact for some functions. + datacenters = find_datacenters(datacenter_name) + datacenters.map do |dc| + @connection.serviceContent.viewManager.CreateContainerView({ + :container => dc.vmFolder, + :type => ["VirtualMachine"], + :recursive => true + }).view + end.flatten + end + def get_folder_path(folder, root = nil) + if (not folder.methods.include?('parent')) or (folder == root) + return + end + "#{get_folder_path(folder.parent)}/#{folder.name}" + end + end + + class Mock + def get_folder_path(folder, root = nil) + nil + end + + def list_virtual_machines(options = { }) + if options['instance_uuid'].nil? and options['mo_ref'].nil? + self.data[:servers].values + elsif !options['instance_uuid'].nil? + server = self.data[:servers][options['instance_uuid']] + server.nil? ? [] : [server] + else + self.data[:servers].values.select{|vm| vm['mo_ref'] == options['mo_ref']} + end + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_vm_customvalues.rb b/lib/fog/vsphere/requests/compute/list_vm_customvalues.rb new file mode 100644 index 00000000..ba2078f3 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_vm_customvalues.rb @@ -0,0 +1,20 @@ +module Fog + module Compute + class Vsphere + class Real + def list_vm_customvalues(vm_id) + get_vm_ref(vm_id).summary.customValue.map do |customvalue| + { + :key => customvalue.key.to_i, + :value => customvalue.value, + } + end + end + end + class Mock + def list_vm_customfields(vm_id) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_vm_interfaces.rb b/lib/fog/vsphere/requests/compute/list_vm_interfaces.rb new file mode 100644 index 00000000..3b1e45dd --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_vm_interfaces.rb @@ -0,0 +1,63 @@ +module Fog + module Compute + class Vsphere + class Real + # => VirtualE1000( + #addressType: "assigned", + #backing: VirtualEthernetCardNetworkBackingInfo( + # deviceName: "VM Network", + # dynamicProperty: [], + # network: Network("network-163"), + # useAutoDetect: false + #), + #connectable: VirtualDeviceConnectInfo( + # allowGuestControl: true, + # connected: true, + # dynamicProperty: [], + # startConnected: true, + # status: "ok" + #), + #controllerKey: 100, + #deviceInfo: Description( + # dynamicProperty: [], + # label: "Network adapter 1", + # summary: "VM Network" + #), + #dynamicProperty: [], + #key: 4000, + #macAddress: "00:50:56:a9:00:28", + #unitNumber: 7, + # + def list_vm_interfaces(vm_id, datacenter = nil) + get_vm_ref(vm_id, datacenter).config.hardware.device.grep(RbVmomi::VIM::VirtualEthernetCard).map do |nic| + { + :name => nic.deviceInfo.label, + :mac => nic.macAddress, + :network => nic.backing.respond_to?("network") ? nic.backing.network.name : nic.backing.port.portgroupKey, + :status => nic.connectable.status, + :summary => nic.deviceInfo.summary, + :type => nic.class, + :key => nic.key, + } + end + end + + def get_vm_interface(vm_id, options={}) + raise ArgumentError, "instance id is a required parameter" unless vm_id + if options.is_a? Fog::Compute::Vsphere::Interface + options + else + raise ArgumentError, "Either key or name is a required parameter. options: #{options}" unless options.key? :key or options.key? :mac or options.key? :name + list_vm_interfaces(vm_id).find do | nic | + (options.key? :key and nic[:key]==options[:key].to_i) or (options.key? :mac and nic[:mac]==options[:mac]) or (options.key? :name and nic[:name]==options[:name]) + end + end + end + end + class Mock + def list_vm_interfaces(vm_id) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/list_vm_volumes.rb b/lib/fog/vsphere/requests/compute/list_vm_volumes.rb new file mode 100644 index 00000000..e59b22e7 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/list_vm_volumes.rb @@ -0,0 +1,52 @@ +module Fog + module Compute + class Vsphere + class Real + # [VirtualDisk( + # backing: VirtualDiskFlatVer2BackingInfo( + # contentId: "a172d19487e878e17d6b16ff2505d7eb", + # datastore: Datastore("datastore-162"), + # diskMode: "persistent", + # dynamicProperty: [], + # fileName: "[Storage1] rhel6-mfojtik/rhel6-mfojtik.vmdk", + # split: false, + # thinProvisioned: true, + # uuid: "6000C29c-a47d-4cd9-5249-c371de775f06", + # writeThrough: false + # ), + # capacityInKB: 8388608, + # controllerKey: 1000, + # deviceInfo: Description( + # dynamicProperty: [], + # label: "Hard disk 1", + # summary: "8,388,608 KB" + # ), + # dynamicProperty: [], + # key: 2001, + # shares: SharesInfo( dynamicProperty: [], level: "normal", shares: 1000 ), + # unitNumber: 1 + #)] + + def list_vm_volumes(vm_id) + get_vm_ref(vm_id).disks.map do |vol| + { + :id => vol.backing.uuid, + :thin => (vol.backing.thinProvisioned rescue(nil)), + :mode => vol.backing.diskMode, + :filename => vol.backing.fileName, + :datastore => (vol.backing.datastore.name rescue(nil)), + :size => vol.capacityInKB, + :name => vol.deviceInfo.label, + :key => vol.key, + :unit_number => vol.unitNumber + } + end + end + end + class Mock + def list_vm_volumes(vm_id) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/modify_vm_interface.rb b/lib/fog/vsphere/requests/compute/modify_vm_interface.rb new file mode 100644 index 00000000..c6744a16 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/modify_vm_interface.rb @@ -0,0 +1,59 @@ +module Fog + module Compute + class Vsphere + class Real + def add_vm_interface(vmid, options = {}) + raise ArgumentError, "instance id is a required parameter" unless vmid + + interface = get_interface_from_options(vmid, options.merge(:server_id => vmid)) + vm_reconfig_hardware('instance_uuid' => vmid, 'hardware_spec' => {'deviceChange'=>[create_interface(interface, 0, :add, options)]}) + end + + def destroy_vm_interface(vmid, options = {}) + raise ArgumentError, "instance id is a required parameter" unless vmid + + interface = get_interface_from_options(vmid, options.merge(:server_id => vmid)) + vm_reconfig_hardware('instance_uuid' => vmid, 'hardware_spec' => {'deviceChange'=>[create_interface(interface, interface.key, :remove)]}) + end + + def update_vm_interface(vmid, options = {}) + raise ArgumentError, "instance id is a required parameter" unless vmid + + interface = get_interface_from_options(vmid, options.merge(:server_id => vmid)) + vm_reconfig_hardware('instance_uuid' => vmid, 'hardware_spec' => {'deviceChange'=>[create_interface(interface, interface.key, :edit)]}) + end + + private + def get_interface_from_options(vmid, options) + if options and options[:interface] + options[:interface] + + elsif options[:key] and options[:key]>0 + oldattributes = get_vm_interface(vmid, options) + Fog::Compute::Vsphere::Interface.new(oldattributes.merge(options)) + + elsif options[:type] and options[:network] + Fog::Compute::Vsphere::Interface.new options + + else + raise ArgumentError, "interface is a required parameter or pass options with type and network" + end + end + end + + class Mock + def add_vm_interface(vmid, options = {}) + raise ArgumentError, "instance id is a required parameter" unless vmid + raise ArgumentError, "interface is a required parameter" unless options and options[:interface] + true + end + + def destroy_vm_interface(vmid, options = {}) + raise ArgumentError, "instance id is a required parameter" unless vmid + raise ArgumentError, "interface is a required parameter" unless options and options[:interface] + true + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/modify_vm_volume.rb b/lib/fog/vsphere/requests/compute/modify_vm_volume.rb new file mode 100644 index 00000000..dce86d1c --- /dev/null +++ b/lib/fog/vsphere/requests/compute/modify_vm_volume.rb @@ -0,0 +1,25 @@ +module Fog + module Compute + class Vsphere + class Real + def add_vm_volume(volume) + vm_reconfig_hardware('instance_uuid' => volume.server_id, 'hardware_spec' => {'deviceChange'=>[create_disk(volume, volume.unit_number, :add)]}) + end + + def destroy_vm_volume(volume) + vm_reconfig_hardware('instance_uuid' => volume.server_id, 'hardware_spec' => {'deviceChange'=>[create_disk(volume, volume.unit_number, :remove)]}) + end + end + + class Mock + def add_vm_volume(volume) + true + end + + def destroy_vm_volume(volume) + true + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/set_vm_customvalue.rb b/lib/fog/vsphere/requests/compute/set_vm_customvalue.rb new file mode 100644 index 00000000..6239ee64 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/set_vm_customvalue.rb @@ -0,0 +1,17 @@ +module Fog + module Compute + class Vsphere + class Real + def set_vm_customvalue(vm_id, key, value) + vm_ref = get_vm_ref(vm_id) + vm_ref.setCustomValue(:key => key, :value => value) + end + end + class Mock + def set_vm_customvalue(vm_id, key, value) + nil + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_clone.rb b/lib/fog/vsphere/requests/compute/vm_clone.rb new file mode 100644 index 00000000..4426f248 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_clone.rb @@ -0,0 +1,727 @@ +module Fog + module Compute + class Vsphere + module Shared + private + def vm_clone_check_options(options) + default_options = { + 'force' => false, + 'linked_clone' => false, + 'nic_type' => 'VirtualE1000', + } + options = default_options.merge(options) + # Backwards compat for "path" option + options["template_path"] ||= options["path"] + options["path"] ||= options["template_path"] + required_options = %w{ datacenter template_path name } + required_options.each do |param| + raise ArgumentError, "#{required_options.join(', ')} are required" unless options.key? param + end + raise Fog::Compute::Vsphere::NotFound, "Datacenter #{options["datacenter"]} Doesn't Exist!" unless get_datacenter(options["datacenter"]) + raise Fog::Compute::Vsphere::NotFound, "Template #{options["template_path"]} Doesn't Exist!" unless get_virtual_machine(options["template_path"], options["datacenter"]) + options + end + end + + class Real + include Shared + + # Clones a VM from a template or existing machine on your vSphere + # Server. + # + # ==== Parameters + # * options<~Hash>: + # * 'datacenter'<~String> - *REQUIRED* Datacenter name your cloning + # in. Make sure this datacenter exists, should if you're using + # the clone function in server.rb model. + # * 'template_path'<~String> - *REQUIRED* The path to the machine you + # want to clone FROM. Relative to Datacenter (Example: + # "FolderNameHere/VMNameHere") + # * 'name'<~String> - *REQUIRED* The VMName of the Destination + # * 'dest_folder'<~String> - Destination Folder of where 'name' will + # be placed on your cluster. Relative Path to Datacenter E.G. + # "FolderPlaceHere/anotherSub Folder/onemore" + # * 'power_on'<~Boolean> - Whether to power on machine after clone. + # Defaults to true. + # * 'wait'<~Boolean> - Whether the method should wait for the virtual + # machine to finish cloning before returning information from + # vSphere. Broken right now as you cannot return a model of a serer + # that isn't finished cloning. Defaults to True + # * 'resource_pool'<~Array> - The resource pool on your datacenter + # cluster you want to use. Only works with clusters within same + # same datacenter as where you're cloning from. Datacenter grabbed + # from template_path option. + # Example: ['cluster_name_here','resource_pool_name_here'] + # * 'datastore'<~String> - The datastore you'd like to use. + # (datacenterObj.datastoreFolder.find('name') in API) + # * 'transform'<~String> - Not documented - see http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.RelocateSpec.html + # * 'numCPUs'<~Integer> - the number of Virtual CPUs of the Destination VM + # * 'memoryMB'<~Integer> - the size of memory of the Destination VM in MB + # * customization_spec<~Hash>: Options are marked as required if you + # use this customization_spec. + # As defined https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Specification.html + # * encryptionKey <~array of bytes> Used to encrypt/decrypt password + # * globalIPSettings expects a hash, REQUIRED + # * identity expects a hash, REQUIRED - either LinuxPrep, Sysprep or SysprepText + # * nicSettingMap expects an array + # * options expects a hash + # * All options can be parsed using a yaml template with cloudinit_to_customspec.rb + # + # OLD Values still supported: + # This only support cloning and setting DHCP on the first interface + # * 'domain'<~String> - *REQUIRED* This is put into + # /etc/resolve.conf (we hope) + # * 'hostname'<~String> - Hostname of the Guest Os - default is + # options['name'] + # * 'hw_utc_clock'<~Boolean> - *REQUIRED* Is hardware clock UTC? + # Default true + # * 'time_zone'<~String> - *REQUIRED* Only valid linux options + # are valid - example: 'America/Denver' + # * 'interfaces' <~Array> - interfaces object to apply to + # the template when cloning: overrides the + # network_label, network_adapter_device_key and nic_type attributes + # * 'volumes' <~Array> - volumes object to apply to + # the template when cloning: this allows to resize the + # existing disks as well as add or remove them. The + # resizing is applied only when the size is bigger then the + # in size in the template + def vm_clone(options = {}) + # Option handling + options = vm_clone_check_options(options) + + # Added for people still using options['path'] + template_path = options['path'] || options['template_path'] + + # Options['template_path']<~String> + # Added for people still using options['path'] + template_path = options['path'] || options['template_path'] + # Now find the template itself using the efficient find method + vm_mob_ref = get_vm_ref(template_path, options['datacenter']) + + # Options['dest_folder']<~String> + # Grab the destination folder object if it exists else use cloned mach + dest_folder_path = options.fetch('dest_folder','/') # default to root path ({dc_name}/vm/) + dest_folder = get_raw_vmfolder(dest_folder_path, options['datacenter']) + + # Options['resource_pool']<~Array> + # Now find _a_ resource pool to use for the clone if one is not specified + if ( options.key?('resource_pool') && options['resource_pool'].is_a?(Array) && options['resource_pool'].length == 2 ) + cluster_name = options['resource_pool'][0] + pool_name = options['resource_pool'][1] + resource_pool = get_raw_resource_pool(pool_name, cluster_name, options['datacenter']) + elsif ( vm_mob_ref.resourcePool == nil ) + # If the template is really a template then there is no associated resource pool, + # so we need to find one using the template's parent host or cluster + esx_host = vm_mob_ref.collect!('runtime.host')['runtime.host'] + # The parent of the ESX host itself is a ComputeResource which has a resourcePool + resource_pool = esx_host.parent.resourcePool + end + # If the vm given did return a valid resource pool, default to using it for the clone. + # Even if specific pools aren't implemented in this environment, we will still get back + # at least the cluster or host we can pass on to the clone task + # This catches if resource_pool option is set but comes back nil and if resourcePool is + # already set. + resource_pool ||= vm_mob_ref.resourcePool.nil? ? esx_host.parent.resourcePool : vm_mob_ref.resourcePool + + # Options['datastore']<~String> + # Grab the datastore object if option is set + datastore_obj = get_raw_datastore(options['datastore'], options['datacenter']) if options.key?('datastore') + # confirm nil if nil or option is not set + datastore_obj ||= nil + virtual_machine_config_spec = RbVmomi::VIM::VirtualMachineConfigSpec() + + device_change = [] + # fully futured interfaces api: replace the current nics + # with the new based on the specification + if (options.key?('interfaces') ) + if options.key?('network_label') + raise ArgumentError, "interfaces option can't be specified together with network_label" + end + device_change.concat(modify_template_nics_specs(template_path, options['interfaces'], options['datacenter'])) + elsif options.key?('network_label') + device_change << modify_template_nics_simple_spec(options['network_label'], options['nic_type'], options['network_adapter_device_key'], options['datacenter']) + end + if disks = options['volumes'] + device_change.concat(modify_template_volumes_specs(vm_mob_ref, options['volumes'])) + end + virtual_machine_config_spec.deviceChange = device_change if device_change.any? + # Options['numCPUs'] or Options['memoryMB'] + # Build up the specification for Hardware, for more details see ____________ + # https://github.com/rlane/rbvmomi/blob/master/test/test_serialization.rb + # http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.ConfigSpec.html + # FIXME: pad this out with the rest of the useful things in VirtualMachineConfigSpec + virtual_machine_config_spec.numCPUs = options['numCPUs'] if ( options.key?('numCPUs') ) + virtual_machine_config_spec.memoryMB = options['memoryMB'] if ( options.key?('memoryMB') ) + virtual_machine_config_spec.cpuHotAddEnabled = options['cpuHotAddEnabled'] if ( options.key?('cpuHotAddEnabled') ) + virtual_machine_config_spec.memoryHotAddEnabled = options['memoryHotAddEnabled'] if ( options.key?('memoryHotAddEnabled') ) + virtual_machine_config_spec.firmware = options['firmware'] if ( options.key?('firmware') ) + # Options['customization_spec'] + # OLD Options still supported + # * domain <~String> - *REQUIRED* - Sets the server's domain for customization + # * dnsSuffixList <~Array> - Optional - Sets the dns search paths in resolv - Example: ["dev.example.com", "example.com"] + # * time_zone <~String> - Required - Only valid linux options are valid - example: 'America/Denver' + # * ipsettings <~Hash> - Optional - If not set defaults to dhcp + # * ip <~String> - *REQUIRED* Sets the ip address of the VM - Example: 10.0.0.10 + # * dnsServerList <~Array> - Optional - Sets the nameservers in resolv - Example: ["10.0.0.2", "10.0.0.3"] + # * gateway <~Array> - Optional - Sets the gateway for the interface - Example: ["10.0.0.1"] + # * subnetMask <~String> - *REQUIRED* - Set the netmask of the interface - Example: "255.255.255.0" + # For other ip settings options see http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.customization.IPSettings.html + # + # Implement complete customization spec as per https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Specification.html + # * encryptionKey <~Array> - Optional, encryption key used to encypt any encrypted passwords + # https://pubs.vmware.com/vsphere-51/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.GlobalIPSettings.html + # * globalIPSettings <~Hash> - REQUIRED + # * dnsServerList <~Array> - Optional, list of dns servers - Example: ["10.0.0.2", "10.0.0.3"] + # * dnsSuffixList <~Array> - Optional, List of name resolution suffixes - Example: ["dev.example.com", "example.com"] + # * identity <~Hash> - REQUIRED, Network identity and settings, similar to Microsoft's Sysprep tool. This is a Sysprep, LinuxPrep, or SysprepText object + # * Sysprep <~Hash> - Optional, representation of a Windows sysprep.inf answer file. + # * guiRunOnce: <~Hash> -Optional, representation of the sysprep GuiRunOnce key + # * commandList: <~Array> - REQUIRED, list of commands to run at first user logon, after guest customization. - Example: ["c:\sysprep\runaftersysprep.cmd", "c:\sysprep\installpuppet.ps1"] + # * guiUnattended: <~Hash> - REQUIRED, representation of the sysprep GuiUnattended key + # * autoLogin: boolean - REQUIRED, Flag to determine whether or not the machine automatically logs on as Administrator. + # * autoLogonCount: int - REQUIRED, specifies the number of times the machine should automatically log on as Administrator + # * password: <~Hash> - REQUIRED, new administrator password for the machine + # * plainText: boolean - REQUIRED, specify whether or not the password is in plain text, rather than encrypted + # * value: <~String> - REQUIRED, password string + # * timeZone: <~int> - REQUIRED, (see here for values https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx) + # * identification: <~Hash> - REQUIRED, representation of the sysprep Identification key + # * domainAdmin: <~String> - Optional, domain user account used for authentication if the virtual machine is joining a domain + # * domainAdminPassword: <~Hash> - Optional, password for the domain user account used for authentication + # * plainText: boolean - REQUIRED, specify whether or not the password is in plain text, rather than encrypted + # * value: <~String> - REQUIRED, password string + # * joinDomain: <~String> - Optional, The domain that the virtual machine should join. If this value is supplied, then domainAdmin and domainAdminPassword must also be supplied + # * joinWorkgroup: <~String> - Optional, The workgroup that the virtual machine should join. + # * licenseFilePrintData: <~Hash> - Optional, representation of the sysprep LicenseFilePrintData key + # * autoMode: <~String> - REQUIRED, Server licensing mode. Two strings are supported: 'perSeat' or 'perServer' + # * autoUsers: <~Int> - Optional, This key is valid only if AutoMode = PerServer. The integer value indicates the number of client licenses + # * userData: <~Hash> - REQUIRED, representation of the sysprep UserData key + # * computerName: <~String> - REQUIRED, The computer name of the (Windows) virtual machine. Will be truncates to 15 characters + # * fullName: <~String> - REQUIRED, User's full name + # * orgName: <~String> - REQUIRED, User's organization + # * productId: <~String> - REQUIRED, serial number for os, ignored if using volume licensed instance + # * LinuxPrep: <~Hash> - Optional, contains machine-wide settings (note the uppercase P) + # * domain: <~String> - REQUIRED, The fully qualified domain name. + # * hostName: <~String> - REQUIRED, the network host name + # * hwClockUTC: <~Boolean> - Optional, Specifies whether the hardware clock is in UTC or local time + # * timeZone: <~String> - Optional, Case sensistive timezone, valid values can be found at https://pubs.vmware.com/vsphere-51/topic/com.vmware.wssdk.apiref.doc/timezone.html + # * SysprepText: <~Hash> - Optional, alternate way to specify the sysprep.inf answer file. + # * value: <~String> - REQUIRED, Text for the sysprep.inf answer file. + # * nicSettingMap: <~Array> - Optional, IP settings that are specific to a particular virtual network adapter + # * Each item in array: + # * adapter: <~Hash> - REQUIRED, IP settings for the associated virtual network adapter + # * dnsDomain: <~String> - Optional, DNS domain suffix for adapter + # * dnsServerList: <~Array> - Optional, list of dns server ip addresses - Example: ["10.0.0.2", "10.0.0.3"] + # * gateway: <~Array> - Optional, list of gateways - Example: ["10.0.0.2", "10.0.0.3"] + # * ip: <~String> - Optional, but required if static IP + # * ipV6Spec: <~Hash> - Optional, IPv^ settings + # * ipAddress: <~String> - Optional, but required if setting static IP + # * gateway: <~Array> - Optional, list of ipv6 gateways + # * netBIOS: <~String> - Optional, NetBIOS settings, if supplied must be one of: disableNetBIOS','enableNetBIOS','enableNetBIOSViaDhcp' + # * primaryWINS: <~String> - Optional, IP address of primary WINS server + # * secondaryWINS: <~String> - Optional, IP address of secondary WINS server + # * subnetMask: <~String> - Optional, subnet mask for adapter + # * macAddress: <~String> - Optional, MAC address of adapter being customized. This cannot be set by the client + # * options: <~Hash> Optional operations, currently only win options have any value + # * changeSID: <~Boolean> - REQUIRED, The customization process should modify the machine's security identifier + # * deleteAccounts: <~Boolean> - REQUIRED, If deleteAccounts is true, then all user accounts are removed from the system + # * reboot: <~String> - Optional, (defaults to reboot), Action to be taken after running sysprep, must be one of: 'noreboot', 'reboot', 'shutdown' + # + if ( options.key?('customization_spec') ) + custom_spec = options['customization_spec'] + + # backwards compatablity + if custom_spec.key?('domain') + # doing this means the old options quash any new ones passed as well... might not be the best way to do it? + # any 'old' options overwrite the following: + # - custom_spec['identity']['LinuxPrep'] + # - custom_spec['globalIPSettings['['dnsServerList'] + # - custom_spec['globalIPSettings']['dnsSuffixList'] + # - custom_spec['nicSettingMap'][0]['adapter']['ip'] + # - custom_spec['nicSettingMap'][0]['adapter']['gateway'] + # - custom_spec['nicSettingMap'][0]['adapter']['subnetMask'] + # - custom_spec['nicSettingMap'][0]['adapter']['dnsDomain'] + # - custom_spec['nicSettingMap'][0]['adapter']['dnsServerList'] + # + # we can assume old parameters being passed + cust_hostname = custom_spec['hostname'] || options['name'] + custom_spec['identity'] = Hash.new unless custom_spec.key?('identity') + custom_spec['identity']['LinuxPrep'] = {"domain" => custom_spec['domain'], "hostName" => cust_hostname, "timeZone" => custom_spec['time_zone']} + + if custom_spec.key?('ipsettings') + custom_spec['globalIPSettings']=Hash.new unless custom_spec.key?('globalIPSettings') + custom_spec['globalIPSettings']['dnsServerList'] = custom_spec['ipsettings']['dnsServerList'] if custom_spec['ipsettings'].key?('dnsServerList') + custom_spec['globalIPSettings']['dnsSuffixList'] = custom_spec['dnsSuffixList'] || [custom_spec['domain']] if ( custom_spec['dnsSuffixList'] || custom_spec['domain']) + end + + if (custom_spec['ipsettings'].key?('ip') or custom_spec['ipsettings'].key?('gateway') or custom_spec['ipsettings'].key?('subnetMask') or custom_spec['ipsettings'].key?('domain') or custom_spec['ipsettings'].key?('dnsServerList')) + if custom_spec['ipsettings'].key?('ip') + raise ArgumentError, "subnetMask is required for static ip" unless custom_spec["ipsettings"].key?("subnetMask") + end + custom_spec['nicSettingMap']=Array.new unless custom_spec.key?('nicSettingMap') + custom_spec['nicSettingMap'][0]=Hash.new unless custom_spec['nicSettingMap'].length > 0 + custom_spec['nicSettingMap'][0]['adapter']=Hash.new unless custom_spec['nicSettingMap'][0].key?('adapter') + custom_spec['nicSettingMap'][0]['adapter']['ip'] = custom_spec['ipsettings']['ip'] if custom_spec['ipsettings'].key?('ip') + custom_spec['nicSettingMap'][0]['adapter']['gateway'] = custom_spec['ipsettings']['gateway'] if custom_spec['ipsettings'].key?('gateway') + custom_spec['nicSettingMap'][0]['adapter']['subnetMask'] = custom_spec['ipsettings']['subnetMask'] if custom_spec['ipsettings'].key?('subnetMask') + custom_spec['nicSettingMap'][0]['adapter']['dnsDomain'] = custom_spec['ipsettings']['domain'] if custom_spec['ipsettings'].key?('domain') + custom_spec['nicSettingMap'][0]['adapter']['dnsServerList'] = custom_spec['ipsettings']['dnsServerList'] if custom_spec['ipsettings'].key?('dnsServerList') + end + end + ### End of backwards compatability + + ## requirements check here ## + raise ArgumentError, "globalIPSettings are required when using Customization Spec" unless custom_spec.key?('globalIPSettings') + raise ArgumentError, "identity is required when using Customization Spec" unless custom_spec.key?('identity') + + # encryptionKey + custom_encryptionKey = custom_spec['encryptionKey'] if custom_spec.key?('encryptionKey') + custom_encryptionKey ||= nil + + # globalIPSettings + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.GlobalIPSettings.html + custom_globalIPSettings = RbVmomi::VIM::CustomizationGlobalIPSettings.new() + custom_globalIPSettings.dnsServerList = custom_spec['globalIPSettings']['dnsServerList'] if custom_spec['globalIPSettings'].key?("dnsServerList") + custom_globalIPSettings.dnsSuffixList = custom_spec['globalIPSettings']['dnsSuffixList'] if custom_spec['globalIPSettings'].key?("dnsSuffixList") + + # identity + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.IdentitySettings.html + # Accepts the 3 supported CustomizationIdentitySettings Types: + # 1. CustomizationLinuxPrep (LinuxPrep) - note the uppercase P + # 2. CustomizationSysprep (Sysprep) + # 3. CustomizationSysprepText (SysprepText) + # At least one of these is required + # + identity = custom_spec['identity'] + if identity.key?("LinuxPrep") + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.LinuxPrep.html + # Fields: + # * domain: string **REQUIRED** + # * hostName: string (CustomizationName) **REQUIRED** Will use options['name'] if not provided. + # * hwClockUTC: boolean + # * timeZone: string (https://pubs.vmware.com/vsphere-55/topic/com.vmware.wssdk.apiref.doc/timezone.html) + raise ArgumentError, "domain is required when using LinuxPrep identity" unless identity['LinuxPrep'].key?('domain') + custom_identity = RbVmomi::VIM::CustomizationLinuxPrep(:domain => identity['LinuxPrep']['domain']) + cust_hostname = RbVmomi::VIM::CustomizationFixedName(:name => identity['LinuxPrep']['hostName']) if identity['LinuxPrep'].key?('hostName') + cust_hostname ||= RbVmomi::VIM::CustomizationFixedName(:name => options['name']) + custom_identity.hostName = cust_hostname + custom_identity.hwClockUTC = identity['LinuxPrep']['hwClockUTC'] if identity['LinuxPrep'].key?('hwClockUTC') + custom_identity.timeZone = identity['LinuxPrep']['timeZone'] if identity['LinuxPrep'].key?('timeZone') + elsif identity.key?("Sysprep") + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Sysprep.html + # Fields: + # * guiRunOnce: CustomizationGuiRunOnce + # * guiUnattended: CustomizationGuiUnattended **REQUIRED** + # * identification: CustomizationIdentification **REQUIRED** + # * licenseFilePrintData: CustomizationLicenseFilePrintData + # * userData: CustomizationUserData **REQUIRED** + # + raise ArgumentError, "guiUnattended is required when using Sysprep identity" unless identity['Sysprep'].key?('guiUnattended') + raise ArgumentError, "identification is required when using Sysprep identity" unless identity['Sysprep'].key?('identification') + raise ArgumentError, "userData is required when using Sysprep identity" unless identity['Sysprep'].key?('userData') + if identity['Sysprep']['guiRunOnce'] + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.GuiRunOnce.html + # Fields: + # * commandList: array of string **REQUIRED*** + # + raise ArgumentError, "commandList is required when using Sysprep identity and guiRunOnce" unless identity['Sysprep']['guiRunOnce'].key?('commandList') + cust_guirunonce = RbVmomi::VIM.CustomizationGuiRunOnce( :commandList => identity['Sysprep']['guiRunOnce']['commandList'] ) + end + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.GuiUnattended.html + # Fields: + # * autoLogin: boolean **REQUIRED** + # * autoLogonCount: int **REQUIRED** + # * timeZone: int (see here for values https://msdn.microsoft.com/en-us/library/ms912391(v=winembedded.11).aspx) **REQUIRED** + # * password: CustomizationPassword + raise ArgumentError, "guiUnattended->autoLogon is required when using Sysprep identity" unless identity['Sysprep']['guiUnattended'].key?('autoLogon') + raise ArgumentError, "guiUnattended->autoLogonCount is required when using Sysprep identity" unless identity['Sysprep']['guiUnattended'].key?('autoLogonCount') + raise ArgumentError, "guiUnattended->timeZone is required when using Sysprep identity" unless identity['Sysprep']['guiUnattended'].key?('timeZone') + custom_guiUnattended = RbVmomi::VIM.CustomizationGuiUnattended( + :autoLogon => identity['Sysprep']['guiUnattended']['autoLogon'], + :autoLogonCount => identity['Sysprep']['guiUnattended']['autoLogonCount'], + :timeZone => identity['Sysprep']['guiUnattended']['timeZone'] + ) + if identity['Sysprep']['guiUnattended']['password'] + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Password.html + # Fields: + # * plainText: boolean **REQUIRED** + # * value: string **REQUIRED** + raise ArgumentError, "guiUnattended->password->plainText is required when using Sysprep identity and guiUnattended -> password" unless identity['Sysprep']['guiUnattended']['password'].key?('plainText') + raise ArgumentError, "guiUnattended->password->value is required when using Sysprep identity and guiUnattended -> password" unless identity['Sysprep']['guiUnattended']['password'].key?('value') + custom_guiUnattended.password = RbVmomi::VIM.CustomizationPassword( + :plainText => identity['Sysprep']['guiUnattended']['password']['plainText'], + :value => identity['Sysprep']['guiUnattended']['password']['value'] + ) + end + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Identification.html + # Fields: + # * domainAdmin: string + # * domainAdminPassword: CustomizationPassword + # * joinDomain: string *If supplied domainAdmin and domainAdminPassword must be set + # * joinWorkgroup: string *If supplied, joinDomain, domainAdmin and domainAdminPassword will be ignored + custom_identification = RbVmomi::VIM.CustomizationIdentification() + if identity['Sysprep']['identification'].key?('joinWorkgroup') + custom_identification.joinWorkgroup = identity['Sysprep']['identification']['joinWorkgroup'] + elsif identity['Sysprep']['identification'].key?('joinDomain') + raise ArgumentError, "identification->domainAdmin is required when using Sysprep identity and identification -> joinDomain" unless identity['Sysprep']['identification'].key?('domainAdmin') + raise ArgumentError, "identification->domainAdminPassword is required when using Sysprep identity and identification -> joinDomain" unless identity['Sysprep']['identification'].key?('domainAdmin') + raise ArgumentError, "identification->domainAdminPassword->plainText is required when using Sysprep identity and identification -> joinDomain" unless identity['Sysprep']['identification']['domainAdminPassword'].key?('plainText') + raise ArgumentError, "identification->domainAdminPassword->value is required when using Sysprep identity and identification -> joinDomain" unless identity['Sysprep']['identification']['domainAdminPassword'].key?('value') + custom_identification.joinDomain = identity['Sysprep']['identification']['joinDomain'] + custom_identification.domainAdmin = identity['Sysprep']['identification']['domainAdmin'] + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Password.html + # Fields: + # * plainText: boolean **REQUIRED** + # * value: string **REQUIRED** + custom_identification.domainAdminPassword = RbVmomi::VIM.CustomizationPassword( + :plainText => identity['Sysprep']['identification']['domainAdminPassword']['plainText'], + :value => identity['Sysprep']['identification']['domainAdminPassword']['value'] + ) + else + raise ArgumentError, "No valid Indentification found, valid values are 'joinWorkgroup' and 'joinDomain'" + end + if identity['Sysprep'].key?('licenseFilePrintData') + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.LicenseFilePrintData.html + # Fields: + # * autoMode: string (CustomizationLicenseDataMode) ** REQUIRED **, valid strings are: 'perSeat' or 'perServer' + # * autoUsers: int (valid only if AutoMode = PerServer) + raise ArgumentError, "licenseFilePrintData->autoMode is required when using Sysprep identity and licenseFilePrintData" unless identity['Sysprep']['licenseFilePrintData'].key?('autoMode') + raise ArgumentError, "Unsupported autoMode, supported modes are : 'perSeat' or 'perServer'" unless ['perSeat', 'perServer'].include? identity['Sysprep']['licenseFilePrintData']['autoMode'] + custom_licenseFilePrintData = RbVmomi::VIM.CustomizationLicenseFilePrintData( + :autoMode => RbVmomi::VIM.CustomizationLicenseDataMode(identity['Sysprep']['licenseFilePrintData']['autoMode']) + ) + if identity['Sysprep']['licenseFilePrintData'].key?('autoUsers') + custom_licenseFilePrintData.autoUsers = identity['Sysprep']['licenseFilePrintData']['autoUsers'] if identity['Sysprep']['licenseFilePrintData']['autoMode'] == "PerServer" + end + end + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.UserData.html + # Fields: + # * computerName: string (CustomizationFixedName) **REQUIRED** + # * fullName: string **REQUIRED** + # * orgName: string **REQUIRED** + # * productID: string **REQUIRED** + raise ArgumentError, "userData->computerName is required when using Sysprep identity" unless identity['Sysprep']['userData'].key?('computerName') + raise ArgumentError, "userData->fullName is required when using Sysprep identity" unless identity['Sysprep']['userData'].key?('fullName') + raise ArgumentError, "userData->orgName is required when using Sysprep identity" unless identity['Sysprep']['userData'].key?('orgName') + raise ArgumentError, "userData->productId is required when using Sysprep identity" unless identity['Sysprep']['userData'].key?('productId') + custom_userData = RbVmomi::VIM.CustomizationUserData( + :fullName => identity['Sysprep']['userData']['fullName'], + :orgName => identity['Sysprep']['userData']['orgName'], + :productId => identity['Sysprep']['userData']['productId'], + :computerName => RbVmomi::VIM.CustomizationFixedName(:name => identity['Sysprep']['userData']['computerName']) + ) + + custom_identity = RbVmomi::VIM::CustomizationSysprep( + :guiUnattended => custom_guiUnattended, + :identification => custom_identification, + :userData => custom_userData + ) + custom_identity.guiRunOnce = cust_guirunonce if defined?(cust_guirunonce) + custom_identity.licenseFilePrintData = custom_licenseFilePrintData if defined?(custom_licenseFilePrintData) + elsif identity.key?("SysprepText") + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.SysprepText.html + # Fields: + # * value: string **REQUIRED** + raise ArgumentError, "SysprepText -> value is required when using SysprepText identity" unless identity['SysprepText'].key?('value') + custom_identity = RbVmomi::VIM::CustomizationSysprepText(:value => identity['SysprepText']['value']) + else + raise ArgumentError, "At least one of the following valid identities must be supplied: LinuxPrep, Sysprep, SysprepText" + end + + if custom_spec.key?("nicSettingMap") + # custom_spec['nicSettingMap'] is an array of adapater mappings: + # custom_spec['nicSettingMap'][0]['macAddress'] + # custom_spec['nicSettingMap'][0]['adapter']['ip'] + #https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.AdapterMapping.html + # Fields: + # * adapter: CustomizationIPSettings **REQUIRED** + # * macAddress: string + raise ArgumentError, "At least one nicSettingMap is required when using nicSettingMap" unless custom_spec['nicSettingMap'].length > 0 + raise ArgumentError, "Adapter is required when using nicSettingMap" unless custom_spec['nicSettingMap'][0].key?('adapter') + + custom_nicSettingMap = [] + # need to go through array here for each apapter + custom_spec['nicSettingMap'].each do | nic | + # https://pubs.vmware.com/vsphere-55/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.customization.IPSettings.html + # Fields: + # * dnsDomain: string + # * gateway: array of string + # * ip: CustomizationIpGenerator (string) **REQUIRED IF Assigning Static IP*** + # * ipV6Spec: CustomizationIPSettingsIpV6AddressSpec + # * netBIOS: CustomizationNetBIOSMode (string) + # * primaryWINS: string + # * secondaryWINS: string + # * subnetMask: string - Required if assigning static IP + if nic['adapter'].key?('ip') + raise ArgumentError, "SubnetMask is required when assigning static IP when using nicSettingMap -> Adapter" unless nic['adapter'].key?('subnetMask') + custom_ip = RbVmomi::VIM.CustomizationFixedIp(:ipAddress => nic['adapter']['ip']) + else + custom_ip = RbVmomi::VIM::CustomizationDhcpIpGenerator.new() + end + custom_adapter = RbVmomi::VIM.CustomizationIPSettings(:ip => custom_ip) + custom_adapter.dnsDomain = nic['adapter']['dnsDomain'] if nic['adapter'].key?('dnsDomain') + custom_adapter.dnsServerList = nic['adapter']['dnsServerList'] if nic['adapter'].key?('dnsServerList') + custom_adapter.gateway = nic['adapter']['gateway'] if nic['adapter'].key?('gateway') + if nic['adapter'].key?('ipV6Spec') + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.IPSettings.IpV6AddressSpec.html + # Fields: + # * gateway: array of string + # * ip: CustomizationIpV6Generator[] **Required if setting static IP ** + if nic['adapter']['ipV6Spec'].key?('ipAddress') + raise ArgumentError, "SubnetMask is required when assigning static IPv6 when using nicSettingMap -> Adapter -> ipV6Spec" unless nic['adapter']['ipV6Spec'].key?('subnetMask') + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.FixedIpV6.html + # * ipAddress: string **REQUIRED** + # * subnetMask: int **REQUIRED** + custom_ipv6 = RbVmomi::VIM.CustomizationFixedIpV6( + :ipAddress => nic['adapter']['ipV6Spec']['ipAddress'], + :subnetMask => nic['adapter']['ipV6Spec']['subnetMask'] + ) + else + custom_ipv6 = RbVmomi::VIM::CustomizationDhcpIpV6Generator.new() + end + custom_ipv6Spec = RbVmomi::VIM.CustomizationIPSettingsIpV6AddressSpec(:ip => custom_ipv6) + custom_ipv6Spec.gateway = nic['adapter']['ipV6Spec']['gateway'] if nic['adapter']['ipV6Spec'].key?('gateway') + custom_adapter.ipV6Spec = custom_ipv6Spec + end + if nic['adapter'].key?('netBIOS') + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.IPSettings.NetBIOSMode.html + # Fields: + # netBIOS: string matching: 'disableNetBIOS','enableNetBIOS' or 'enableNetBIOSViaDhcp' ** REQUIRED ** + # + raise ArgumentError, "Unsupported NetBIOSMode, supported modes are : 'disableNetBIOS','enableNetBIOS' or 'enableNetBIOSViaDhcp'" unless ['disableNetBIOS','enableNetBIOS','enableNetBIOSViaDhcp'].include? nic['adapter']['netBIOS'] + custom_adapter.netBIOS = RbVmomi::VIM.CustomizationNetBIOSMode(nic['adapter']['netBIOS']) + end + custom_adapter.primaryWINS = nic['adapter']['primaryWINS'] if nic['adapter'].key?('primaryWINS') + custom_adapter.secondaryWINS = nic['adapter']['secondaryWINS'] if nic['adapter'].key?('secondaryWINS') + custom_adapter.subnetMask = nic['adapter']['subnetMask'] if nic['adapter'].key?('subnetMask') + + custom_adapter_mapping = RbVmomi::VIM::CustomizationAdapterMapping(:adapter => custom_adapter) + custom_adapter_mapping.macAddress = nic['macAddress'] if nic.key?('macAddress') + + # build the adapters array, creates it if not already created, otherwise appends to it + custom_nicSettingMap << custom_adapter_mapping + end + end + custom_nicSettingMap = nil if custom_nicSettingMap.length < 1 + + if custom_spec.key?("options") + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Options.html + # this currently doesn't have any Linux options, just windows + # Fields: + # * changeSID: boolean **REQUIRED** + # * deleteAccounts: boolean **REQUIRED** **note deleteAccounts is deprecated as of VI API 2.5 so can be ignored + # * reboot: CustomizationSysprepRebootOption: (string) one of following 'noreboot', reboot' or 'shutdown' (defaults to reboot) + raise ArgumentError, "changeSID id required when using Windows Options" unless custom_spec['options'].key?('changeSID') + raise ArgumentError, "deleteAccounts id required when using Windows Options" unless custom_spec['options'].key?('deleteAccounts') + custom_options = RbVmomi::VIM::CustomizationWinOptions( + :changeSID => custom_spec['options']['changeSID'], + :deleteAccounts => custom_spec['options']['deleteAccounts'] + ) + if custom_spec['options'].key?('reboot') + raise ArgumentError, "Unsupported reboot option, supported options are : 'noreboot', 'reboot' or 'shutdown'" unless ['noreboot','reboot','shutdown'].include? custom_spec['options']['reboot'] + custom_options.reboot = RBVmomi::VIM.CustomizationSysprepRebootOption(custom_spec['options']['reboot']) + end + end + custom_options ||=nil + + # https://pubs.vmware.com/vsphere-55/index.jsp#com.vmware.wssdk.apiref.doc/vim.vm.customization.Specification.html + customization_spec = RbVmomi::VIM::CustomizationSpec( + :globalIPSettings => custom_globalIPSettings, + :identity => custom_identity + ) + customization_spec.encryptionKey = custom_encryptionKey if defined?(custom_encryptionKey) + customization_spec.nicSettingMap = custom_nicSettingMap if defined?(custom_nicSettingMap) + customization_spec.options = custom_options if defined?(custom_options) + + end + customization_spec ||= nil + + relocation_spec=nil + if ( options['linked_clone'] ) + # cribbed heavily from the rbvmomi clone_vm.rb + # this chunk of code reconfigures the disk of the clone source to be read only, + # and then creates a delta disk on top of that, this is required by the API in order to create + # linked clondes + disks = vm_mob_ref.config.hardware.device.select do |vm_device| + vm_device.class == RbVmomi::VIM::VirtualDisk + end + disks.select{|vm_device| vm_device.backing.parent == nil}.each do |disk| + disk_spec = { + :deviceChange => [ + { + :operation => :remove, + :device => disk + }, + { + :operation => :add, + :fileOperation => :create, + :device => disk.dup.tap{|disk_backing| + disk_backing.backing = disk_backing.backing.dup; + disk_backing.backing.fileName = "[#{disk.backing.datastore.name}]"; + disk_backing.backing.parent = disk.backing + } + } + ] + } + vm_mob_ref.ReconfigVM_Task(:spec => disk_spec).wait_for_completion + end + # Next, create a Relocation Spec instance + relocation_spec = RbVmomi::VIM.VirtualMachineRelocateSpec(:datastore => datastore_obj, + :pool => resource_pool, + :diskMoveType => :moveChildMostDiskBacking) + else + relocation_spec = RbVmomi::VIM.VirtualMachineRelocateSpec(:datastore => datastore_obj, + :pool => resource_pool, + :transform => options['transform'] || 'sparse') + end + # And the clone specification + clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => relocation_spec, + :config => virtual_machine_config_spec, + :customization => customization_spec, + :powerOn => options.key?('power_on') ? options['power_on'] : true, + :template => false) + + # Perform the actual Clone Task + task = vm_mob_ref.CloneVM_Task(:folder => dest_folder, + :name => options['name'], + :spec => clone_spec) + # Waiting for the VM to complete allows us to get the VirtulMachine + # object of the new machine when it's done. It is HIGHLY recommended + # to set 'wait' => true if your app wants to wait. Otherwise, you're + # going to have to reload the server model over and over which + # generates a lot of time consuming API calls to vmware. + if options.fetch('wait', true) then + # REVISIT: It would be awesome to call a block passed to this + # request to notify the application how far along in the process we + # are. I'm thinking of updating a progress bar, etc... + new_vm = task.wait_for_completion + else + tries = 0 + new_vm = begin + # Try and find the new VM (folder.find is quite efficient) + dest_folder.find(options['name'], RbVmomi::VIM::VirtualMachine) or raise Fog::Vsphere::Errors::NotFound + rescue Fog::Vsphere::Errors::NotFound + tries += 1 + if tries <= 10 then + sleep 15 + retry + end + nil + end + end + + # Return hash + { + 'vm_ref' => new_vm ? new_vm._ref : nil, + 'new_vm' => new_vm ? convert_vm_mob_ref_to_attr_hash(new_vm) : nil, + 'task_ref' => task._ref + } + end + + # Build up the network config spec for simple case: + # simple case: apply just the network_label, nic_type and network_adapter_device_key + def modify_template_nics_simple_spec(network_label, nic_type, network_adapter_device_key, datacenter) + config_spec_operation = RbVmomi::VIM::VirtualDeviceConfigSpecOperation('edit') + # Get the portgroup and handle it from there. + network = get_raw_network(network_label, datacenter) + if ( network.kind_of? RbVmomi::VIM::DistributedVirtualPortgroup) + # Create the NIC backing for the distributed virtual portgroup + nic_backing_info = RbVmomi::VIM::VirtualEthernetCardDistributedVirtualPortBackingInfo( + :port => RbVmomi::VIM::DistributedVirtualSwitchPortConnection( + :portgroupKey => network.key, + :switchUuid => network.config.distributedVirtualSwitch.uuid + ) + ) + else + # Otherwise it's a non distributed port group + nic_backing_info = RbVmomi::VIM::VirtualEthernetCardNetworkBackingInfo(:deviceName => network_label) + end + connectable = RbVmomi::VIM::VirtualDeviceConnectInfo( + :allowGuestControl => true, + :connected => true, + :startConnected => true) + device = RbVmomi::VIM.public_send "#{nic_type}", + :backing => nic_backing_info, + :deviceInfo => RbVmomi::VIM::Description(:label => "Network adapter 1", :summary => network_label), + :key => network_adapter_device_key, + :connectable => connectable + device_spec = RbVmomi::VIM::VirtualDeviceConfigSpec( + :operation => config_spec_operation, + :device => device) + return device_spec + end + + + def modify_template_nics_specs(template_path, new_nics, datacenter) + template_nics = list_vm_interfaces(template_path, datacenter).map do |old_attributes| + Fog::Compute::Vsphere::Interface.new(old_attributes) + end + specs = [] + + template_nics.each do |interface| + specs << create_interface(interface, interface.key, :remove, :datacenter => datacenter) + end + + new_nics.each do |interface| + specs << create_interface(interface, 0, :add, :datacenter => datacenter) + end + + return specs + end + + def modify_template_volumes_specs(vm_mob_ref, volumes) + template_volumes = vm_mob_ref.config.hardware.device.grep(RbVmomi::VIM::VirtualDisk) + modified_volumes = volumes.take(template_volumes.size) + new_volumes = volumes.drop(template_volumes.size) + + specs = [] + template_volumes.zip(modified_volumes).each do |template_volume, new_volume| + if new_volume + # updated the attribtues on the existing volume + # it's not allowed to reduce the size of the volume when cloning + if new_volume.size > template_volume.capacityInKB + template_volume.capacityInKB = new_volume.size + end + template_volume.backing.diskMode = new_volume.mode + template_volume.backing.thinProvisioned = new_volume.thin + specs << { :operation => :edit, :device => template_volume } + else + specs << { :operation => :remove, + :fileOperation => :destroy, + :device => template_volume } + end + end + specs.concat(new_volumes.map { |volume| create_disk(volume, volumes.index(volume)) }) + return specs + end + end + + class Mock + include Shared + def vm_clone(options = {}) + # Option handling TODO Needs better method of checking + options = vm_clone_check_options(options) + notfound = lambda { raise Fog::Compute::Vsphere::NotFound, "Could not find VM template" } + template = list_virtual_machines.find(notfound) do |vm| + vm['name'] == options['template_path'].split("/")[-1] + end + + # generate a random id + id = [8,4,4,4,12].map{|i| Fog::Mock.random_hex(i)}.join("-") + new_vm = template.clone.merge({ + "name" => options['name'], + "id" => id, + "instance_uuid" => id, + "path" => "/Datacenters/#{options['datacenter']}/#{options['dest_folder'] ? options['dest_folder']+"/" : ""}#{options['name']}" + }) + self.data[:servers][id] = new_vm + + { + 'vm_ref' => "vm-#{Fog::Mock.random_numbers(3)}", + 'new_vm' => new_vm, + 'task_ref' => "task-#{Fog::Mock.random_numbers(4)}", + } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_config_vnc.rb b/lib/fog/vsphere/requests/compute/vm_config_vnc.rb new file mode 100644 index 00000000..ddfa0eab --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_config_vnc.rb @@ -0,0 +1,45 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_config_vnc(options = { }) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + + search_filter = { :uuid => options['instance_uuid'], 'vmSearch' => true, 'instanceUuid' => true } + vm_mob_ref = @connection.searchIndex.FindAllByUuid(search_filter).first + task = vm_mob_ref.ReconfigVM_Task(:spec => { + :extraConfig => [ + { :key => 'RemoteDisplay.vnc.enabled', :value => options[:enabled] ? 'true' : 'false' }, + { :key => 'RemoteDisplay.vnc.password', :value => options[:password].to_s }, + { :key => 'RemoteDisplay.vnc.port', :value => options[:port].to_s || '5910' } + ] + }) + task.wait_for_completion + { 'task_state' => task.info.state } + end + + # return a hash of VNC attributes required to view the console + def vm_get_vnc uuid + search_filter = { :uuid => uuid, 'vmSearch' => true, 'instanceUuid' => true } + vm = @connection.searchIndex.FindAllByUuid(search_filter).first + Hash[vm.config.extraConfig.map do |config| + if config.key =~ /^RemoteDisplay\.vnc\.(\w+)$/ + [$1.to_sym, config.value] + end + end.compact] + end + end + + class Mock + def vm_config_vnc(options = { }) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + { 'task_state' => 'success' } + end + + def vm_get_vnc uuid + {:password => 'secret', :port => '5900', :enabled => 'true'} + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_destroy.rb b/lib/fog/vsphere/requests/compute/vm_destroy.rb new file mode 100644 index 00000000..200889e0 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_destroy.rb @@ -0,0 +1,23 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_destroy(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + + vm_mob_ref = get_vm_ref(options['instance_uuid']) + task = vm_mob_ref.Destroy_Task + task.wait_for_completion + { 'task_state' => task.info.state } + end + end + + class Mock + def vm_destroy(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + { 'task_state' => 'success' } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_execute.rb b/lib/fog/vsphere/requests/compute/vm_execute.rb new file mode 100644 index 00000000..5ab2ad3a --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_execute.rb @@ -0,0 +1,47 @@ +module Fog + module Compute + class Vsphere + class Real + # NOTE: you must be using vsphere_rev 5.0 or greater to use this functionality + # e.g. Fog::Compute.new(provider: "vsphere", vsphere_rev: "5.5", etc) + # * options<~Hash>: + # * 'instance_uuid'<~String> - *REQUIRED* the instance uuid you would like to operate on + # * 'command'<~String> *REQUIRED* the command to execute + # * 'args'<~String> arguments to pass the command + # * 'working_dir'<~String> path to the working directory + # * 'user'<~String> *REQUIRED* the ssh username you would like to login as + # * 'password'<~String> *REQUIRED* the ssh password for the user you would like to log in as + def vm_execute(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + raise ArgumentError, "command is a required parameter" unless options.key? 'command' + raise ArgumentError, "user is a required parameter" unless options.key? 'user' + raise ArgumentError, "password is a required parameter" unless options.key? 'password' + + search_filter = { :uuid => options['instance_uuid'], 'vmSearch' => true, 'instanceUuid' => true } + vm_mob_ref = @connection.searchIndex.FindAllByUuid(search_filter).first + + auth = RbVmomi::VIM::NamePasswordAuthentication(:interactiveSession => false, + :username => options['user'], + :password => options['password']) + + spec = RbVmomi::VIM::GuestProgramSpec(:programPath => options['command'], + :arguments => options['args'], + :workingDirectory => options['working_dir']) + + gom = @connection.serviceContent.guestOperationsManager + gom.processManager.StartProgramInGuest(:vm => vm_mob_ref, :auth => auth, :spec => spec) + end + end + + class Mock + def vm_execute(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + raise ArgumentError, "command is a required parameter" unless options.key? 'command' + raise ArgumentError, "user is a required parameter" unless options.key? 'user' + raise ArgumentError, "password is a required parameter" unless options.key? 'password' + return 12345 + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_migrate.rb b/lib/fog/vsphere/requests/compute/vm_migrate.rb new file mode 100644 index 00000000..c4a422a5 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_migrate.rb @@ -0,0 +1,33 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_migrate(options = {}) + #priority is the only required option, and it has a sane default option. + priority = options['priority'].nil? ? 'defaultPriority' : options["priority"] + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + + # Find the VM Object + search_filter = { :uuid => options['instance_uuid'], 'vmSearch' => true, 'instanceUuid' => true } + vm_mob_ref = @connection.searchIndex.FindAllByUuid(search_filter).first + + unless vm_mob_ref.kind_of? RbVmomi::VIM::VirtualMachine + raise Fog::Vsphere::Errors::NotFound, + "Could not find VirtualMachine with instance uuid #{options['instance_uuid']}" + end + task = vm_mob_ref.MigrateVM_Task(:pool => options['pool'], :host => options['host'], :priority => "#{priority}", :state => options['state'] ) + task.wait_for_completion + { 'task_state' => task.info.state } + end + end + + class Mock + def vm_migrate(options = {}) + priority = options['priority'].nil? ? 'defaultPriority' : options["priority"] + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + { 'task_state' => 'success' } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_power_off.rb b/lib/fog/vsphere/requests/compute/vm_power_off.rb new file mode 100644 index 00000000..fc954d35 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_power_off.rb @@ -0,0 +1,39 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_power_off(options = {}) + options = { 'force' => false }.merge(options) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + + search_filter = { :uuid => options['instance_uuid'], 'vmSearch' => true, 'instanceUuid' => true } + vm_mob_ref = @connection.searchIndex.FindAllByUuid(search_filter).first + + if options['force'] then + task = vm_mob_ref.PowerOffVM_Task + task.wait_for_completion + { 'task_state' => task.info.result, 'power_off_type' => 'cut_power' } + else + vm_mob_ref.ShutdownGuest + { + 'task_state' => "running", + 'power_off_type' => 'shutdown_guest', + } + end + end + end + + class Mock + def vm_power_off(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + vm = get_virtual_machine(options['instance_uuid']) + vm["power_state"] = "poweredOff" + { + 'task_state' => "running", + 'power_off_type' => options['force'] ? 'cut_power' : 'shutdown_guest', + } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_power_on.rb b/lib/fog/vsphere/requests/compute/vm_power_on.rb new file mode 100644 index 00000000..40059cb6 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_power_on.rb @@ -0,0 +1,26 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_power_on(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + + search_filter = { :uuid => options['instance_uuid'], 'vmSearch' => true, 'instanceUuid' => true } + vm_mob_ref = @connection.searchIndex.FindAllByUuid(search_filter).first + + task = vm_mob_ref.PowerOnVM_Task + task.wait_for_completion + # 'success', 'running', 'queued', 'error' + { 'task_state' => task.info.state } + end + end + + class Mock + def vm_power_on(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + { 'task_state' => 'success' } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_reboot.rb b/lib/fog/vsphere/requests/compute/vm_reboot.rb new file mode 100644 index 00000000..7d396821 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_reboot.rb @@ -0,0 +1,31 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_reboot(options = {}) + options = { 'force' => false }.merge(options) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + + search_filter = { :uuid => options['instance_uuid'], 'vmSearch' => true, 'instanceUuid' => true } + vm_mob_ref = @connection.searchIndex.FindAllByUuid(search_filter).first + + if options['force'] then + task = vm_mob_ref.ResetVM_Task + task.wait_for_completion + { 'task_state' => task.info.result, 'reboot_type' => 'reset_power' } + else + vm_mob_ref.RebootGuest + { 'task_state' => "running", 'reboot_type' => 'reboot_guest' } + end + end + end + + class Mock + def vm_reboot(options = {}) + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + { 'task_state' => "running", 'reboot_type' => options['force'] ? 'reset_power' : 'reboot_guest' } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_reconfig_cpus.rb b/lib/fog/vsphere/requests/compute/vm_reconfig_cpus.rb new file mode 100644 index 00000000..ffff6bb0 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_reconfig_cpus.rb @@ -0,0 +1,23 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_reconfig_cpus(options = {}) + raise ArgumentError, "cpus is a required parameter" unless options.key? 'cpus' + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + hardware_spec={'numCPUs' => options['cpus'], 'numCoresPerSocket' => options['corespersocket']} + vm_reconfig_hardware('instance_uuid' => options['instance_uuid'], 'hardware_spec' => hardware_spec ) + end + end + + class Mock + def vm_reconfig_cpus(options = {}) + raise ArgumentError, "cpus is a required parameter" unless options.key? 'cpus' + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + hardware_spec={'numCPUs' => options['cpus'], 'numCoresPerSocket' => options['corespersocket']} + vm_reconfig_hardware('instance_uuid' => options['instance_uuid'], 'hardware_spec' => hardware_spec ) + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_reconfig_hardware.rb b/lib/fog/vsphere/requests/compute/vm_reconfig_hardware.rb new file mode 100644 index 00000000..a491b9c0 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_reconfig_hardware.rb @@ -0,0 +1,24 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_reconfig_hardware(options = {}) + raise ArgumentError, "hardware_spec is a required parameter" unless options.key? 'hardware_spec' + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + vm_mob_ref = get_vm_ref(options['instance_uuid']) + task = vm_mob_ref.ReconfigVM_Task(:spec => RbVmomi::VIM.VirtualMachineConfigSpec(options['hardware_spec'])) + task.wait_for_completion + { 'task_state' => task.info.state } + end + end + + class Mock + def vm_reconfig_hardware(options = {}) + raise ArgumentError, "hardware_spec is a required parameter" unless options.key? 'hardware_spec' + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + { 'task_state' => 'success' } + end + end + end + end +end diff --git a/lib/fog/vsphere/requests/compute/vm_reconfig_memory.rb b/lib/fog/vsphere/requests/compute/vm_reconfig_memory.rb new file mode 100644 index 00000000..fef32841 --- /dev/null +++ b/lib/fog/vsphere/requests/compute/vm_reconfig_memory.rb @@ -0,0 +1,23 @@ +module Fog + module Compute + class Vsphere + class Real + def vm_reconfig_memory(options = {}) + raise ArgumentError, "memory is a required parameter" unless options.key? 'memory' + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + hardware_spec={'memoryMB' => options['memory']} + vm_reconfig_hardware('instance_uuid' => options['instance_uuid'], 'hardware_spec' => hardware_spec ) + end + end + + class Mock + def vm_reconfig_memory(options = {}) + raise ArgumentError, "memory is a required parameter" unless options.key? 'memory' + raise ArgumentError, "instance_uuid is a required parameter" unless options.key? 'instance_uuid' + hardware_spec={'memoryMB' => options['memory']} + vm_reconfig_hardware('instance_uuid' => options['instance_uuid'], 'hardware_spec' => hardware_spec ) + end + end + end + end +end diff --git a/lib/fog/vsphere/version.rb b/lib/fog/vsphere/version.rb new file mode 100644 index 00000000..9d76b9e1 --- /dev/null +++ b/lib/fog/vsphere/version.rb @@ -0,0 +1,5 @@ +module Fog + module Vsphere + VERSION = "0.1.0" + end +end diff --git a/tests/compute_tests.rb b/tests/compute_tests.rb new file mode 100644 index 00000000..70d62743 --- /dev/null +++ b/tests/compute_tests.rb @@ -0,0 +1,53 @@ +Shindo.tests('Fog::Compute[:vsphere]', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + tests("| convert_vm_mob_ref_to_attr_hash") do + # Mock the RbVmomi::VIM::ManagedObject class + class MockManagedObject + attr_reader :parent, :_ref + + def initialize + @parent = @_ref = 'vm-123' + end + + def collect! *pathSet + { '_ref' => 'vm-123', 'name' => 'fakevm' } + end + end + + fake_vm_mob_ref = MockManagedObject.new + + tests("When converting an incomplete vm object") do + test("it should return a Hash") do + compute.send(:convert_vm_mob_ref_to_attr_hash, fake_vm_mob_ref).kind_of? Hash + end + tests("The converted Hash should") do + attr_hash = compute.send(:convert_vm_mob_ref_to_attr_hash, fake_vm_mob_ref) + test("have a name") { attr_hash['name'] == 'fakevm' } + test("have a mo_ref") {attr_hash['mo_ref'] == 'vm-123' } + test("have an id") { attr_hash['id'] == 'vm-123' } + test("not have a instance_uuid") { attr_hash['instance_uuid'].nil? } + end + end + + tests("When passed a nil object") do + attr_hash = compute.send :convert_vm_mob_ref_to_attr_hash, nil + test("it should return a nil object") do + attr_hash.nil? + end + end + end + + tests("Compute attributes") do + %w{ vsphere_is_vcenter vsphere_rev vsphere_username vsphere_server }.each do |attr| + test("it should respond to #{attr}") { compute.respond_to? attr } + end + end + + tests("Compute collections") do + %w{ servers }.each do |collection| + test("it should respond to #{collection}") { compute.respond_to? collection } + end + end +end diff --git a/tests/helper.rb b/tests/helper.rb new file mode 100644 index 00000000..8e60fb21 --- /dev/null +++ b/tests/helper.rb @@ -0,0 +1 @@ +require File.expand_path('../../lib/fog/vsphere', __FILE__) diff --git a/tests/helpers/mock_helper.rb b/tests/helpers/mock_helper.rb new file mode 100644 index 00000000..6c4a18cd --- /dev/null +++ b/tests/helpers/mock_helper.rb @@ -0,0 +1,9 @@ +Fog.mock! if ENV['FOG_MOCK'] == 'true' + +if Fog.mock? + Fog.credentials = { + :vsphere_server => 'fake_vsphere_server', + :vsphere_username => 'fake_vsphere_username', + :vsphere_password => 'fake_vsphere_password' + }.merge(Fog.credentials) +end diff --git a/tests/helpers/succeeds_helper.rb b/tests/helpers/succeeds_helper.rb new file mode 100644 index 00000000..b54589e3 --- /dev/null +++ b/tests/helpers/succeeds_helper.rb @@ -0,0 +1,9 @@ +module Shindo + class Tests + def succeeds + test('succeeds') do + !!instance_eval(&Proc.new) + end + end + end +end diff --git a/tests/models/compute/server_tests.rb b/tests/models/compute/server_tests.rb new file mode 100644 index 00000000..4f692f75 --- /dev/null +++ b/tests/models/compute/server_tests.rb @@ -0,0 +1,44 @@ +Shindo.tests('Fog::Compute[:vsphere] | server model', ['vsphere']) do + + servers = Fog::Compute[:vsphere].servers + server = servers.last + + tests('The server model should') do + tests('have the action') do + test('reload') { server.respond_to? 'reload' } + %w{ stop start destroy reboot }.each do |action| + test(action) { server.respond_to? action } + test("#{action} returns successfully") { server.send(action.to_sym) ? true : false } + end + end + tests('have attributes') do + model_attribute_hash = server.attributes + attributes = [ :id, + :instance_uuid, + :uuid, + :power_state, + :tools_state, + :mo_ref, + :tools_version, + :hostname, + :mac_addresses, + :operatingsystem, + :connection_state, + :hypervisor, + :name, + :public_ip_address] + tests("The server model should respond to") do + attributes.each do |attribute| + test("#{attribute}") { server.respond_to? attribute } + end + end + tests("The attributes hash should have key") do + attributes.each do |attribute| + test("#{attribute}") { model_attribute_hash.key? attribute } + end + end + end + test('be a kind of Fog::Compute::Vsphere::Server') { server.kind_of? Fog::Compute::Vsphere::Server } + end + +end diff --git a/tests/models/compute/servers_tests.rb b/tests/models/compute/servers_tests.rb new file mode 100644 index 00000000..c0e004a0 --- /dev/null +++ b/tests/models/compute/servers_tests.rb @@ -0,0 +1,15 @@ +Shindo.tests('Fog::Compute[:vsphere] | servers collection', ['vsphere']) do + + servers = Fog::Compute[:vsphere].servers + + tests('The servers collection') do + test('should not be empty') { not servers.empty? } + test('should be a kind of Fog::Compute::Vsphere::Servers') { servers.kind_of? Fog::Compute::Vsphere::Servers } + tests('should be able to reload itself').succeeds { servers.reload } + tests('should be able to get a model') do + tests('by managed object reference').succeeds { servers.get 'vm-715' } + tests('by instance uuid').succeeds { servers.get '5032c8a5-9c5e-ba7a-3804-832a03e16381' } + end + end + +end diff --git a/tests/requests/compute/current_time_tests.rb b/tests/requests/compute/current_time_tests.rb new file mode 100644 index 00000000..a19755da --- /dev/null +++ b/tests/requests/compute/current_time_tests.rb @@ -0,0 +1,12 @@ +Shindo.tests('Fog::Compute[:vsphere] | current_time request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + tests('The response should') do + response = compute.current_time + test('be a kind of Hash') { response.kind_of? Hash } + test('have a current_time key') { response.key? 'current_time' } + test('have a current_time key with a Time value') { response['current_time'].kind_of? Time } + end + +end diff --git a/tests/requests/compute/get_network_tests.rb b/tests/requests/compute/get_network_tests.rb new file mode 100644 index 00000000..795b0bc7 --- /dev/null +++ b/tests/requests/compute/get_network_tests.rb @@ -0,0 +1,50 @@ +require 'ostruct' + +Shindo.tests('Fog::Compute[:vsphere] | get_network request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + + class DistributedVirtualPortgroup + attr_accessor :name, :dvs_name + + def initialize attrs + @name = attrs.fetch(:name) + @dvs_name = attrs.fetch(:dvs_name) + end + + def config + OpenStruct.new( :distributedVirtualSwitch => OpenStruct.new(:name => dvs_name)) + end + end + + fake_networks = [OpenStruct.new(:name => 'non-dvs'), + DistributedVirtualPortgroup.new( :name => 'web1', :dvs_name => 'dvs5'), + DistributedVirtualPortgroup.new( :name => 'web1', :dvs_name => 'dvs11'), + DistributedVirtualPortgroup.new( :name => 'other', :dvs_name => 'other'), + ] + + + tests('#choose_finder should') do + test('choose the network based on network name and dvs name'){ + finder = compute.send(:choose_finder, 'web1', 'dvs11') + found_network = fake_networks.find{ |n| finder.call(n) } + found_network.name == 'web1' && found_network.dvs_name == 'dvs11' + } + test('choose the network based on network name and any dvs'){ + finder = compute.send(:choose_finder, 'web1', :dvs) + found_network = fake_networks.find{ |n| finder.call(n) } + found_network.name == 'web1' && found_network.dvs_name == 'dvs5' + } + test('choose the network based on network name only'){ + finder = compute.send(:choose_finder, 'other', nil) + found_network = fake_networks.find{ |n| finder.call(n) } + found_network.name == 'other' && found_network.dvs_name == 'other' + } + test('choose the network based on network name only for non-dvs'){ + finder = compute.send(:choose_finder, 'non-dvs', nil) + found_network = fake_networks.find{ |n| finder.call(n) } + found_network.name == 'non-dvs' && found_network.class.name.to_s == 'OpenStruct' + } + end +end diff --git a/tests/requests/compute/list_clusters_tests.rb b/tests/requests/compute/list_clusters_tests.rb new file mode 100644 index 00000000..cf1e959e --- /dev/null +++ b/tests/requests/compute/list_clusters_tests.rb @@ -0,0 +1,11 @@ +Shindo.tests('Fog::Compute[:vsphere] | list_clusters request', ['vsphere']) do + tests("When listing all clusters") do + + response = Fog::Compute[:vsphere].list_clusters + test("Clusters extracted from folders... ") {response.length == 4} + + tests("The response data format ...") do + test("be a kind of Hash") { response.kind_of? Array } + end + end +end \ No newline at end of file diff --git a/tests/requests/compute/list_virtual_machines_tests.rb b/tests/requests/compute/list_virtual_machines_tests.rb new file mode 100644 index 00000000..6f572a79 --- /dev/null +++ b/tests/requests/compute/list_virtual_machines_tests.rb @@ -0,0 +1,38 @@ +Shindo.tests('Fog::Compute[:vsphere] | list_virtual_machines request', ['vsphere']) do + + tests("When listing all machines") do + + response = Fog::Compute[:vsphere].list_virtual_machines + + tests("The response data format ...") do + test("be a kind of Hash") { response.kind_of? Array } + end + end + + tests("When providing an instance_uuid") do + + # pending unless Fog.mock? + + tests("that does exist") do + uuid = "5029c440-85ee-c2a1-e9dd-b63e39364603" + response = Fog::Compute[:vsphere].list_virtual_machines({'instance_uuid' => uuid}) + + tests("The response should") do + test("contain one vm") { response.length == 1 } + test("contain that is an attribute hash") { response[0].kind_of? Hash } + test("find jefftest") { response.first['name'] == 'jefftest' } + end + end + + tests("that does not exist or is a template") do + %w{ does-not-exist-and-is-not-a-uuid 50323f93-6835-1178-8b8f-9e2109890e1a }.each do |uuid| + response = Fog::Compute[:vsphere].list_virtual_machines({'instance_uuid' => uuid}) + + tests("The response should") do + test("be empty") { response.empty? } + end + end + end + end + +end diff --git a/tests/requests/compute/set_vm_customvalue_tests.rb b/tests/requests/compute/set_vm_customvalue_tests.rb new file mode 100644 index 00000000..7adcebe3 --- /dev/null +++ b/tests/requests/compute/set_vm_customvalue_tests.rb @@ -0,0 +1,20 @@ +Shindo.tests('Fog::Compute[:vsphere] | set_vm_customvalue request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + instance_uuid = '50137835-88a1-436e-768e-9b2677076e67' + custom_key = nil + custom_value = nil + + tests('The response should') do + response = compute.set_vm_customvalue(instance_uuid, custom_key, custom_value) + test('be nil') { response.nil? } + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.set_vm_customvalue } + raises(ArgumentError, 'raises ArgumentError when custom_key option is missing') { compute.set_vm_customvalue(instance_uuid) } + raises(ArgumentError, 'raises ArgumentError when custom_value option is missing') { compute.set_vm_customvalue(instance_uuid, custom_key) } + end + +end diff --git a/tests/requests/compute/vm_clone_tests.rb b/tests/requests/compute/vm_clone_tests.rb new file mode 100644 index 00000000..5d8abc5d --- /dev/null +++ b/tests/requests/compute/vm_clone_tests.rb @@ -0,0 +1,50 @@ +Shindo.tests("Fog::Compute[:vsphere] | vm_clone request", 'vsphere') do + # require 'guid' + compute = Fog::Compute[:vsphere] + response = nil + response_linked = nil + + template = "rhel64" + datacenter = "Solutions" + + tests("Standard Clone | The return value should") do + servers_size = compute.servers.size + response = compute.vm_clone('datacenter' => datacenter, 'template_path' => template, 'name' => 'cloning_vm', 'wait' => true) + test("be a kind of Hash") { response.kind_of? Hash } + %w{ vm_ref new_vm task_ref }.each do |key| + test("have a #{key} key") { response.key? key } + end + test("creates a new server") { compute.servers.size == servers_size+1 } + test("new server name is set") { compute.get_virtual_machine(response['new_vm']['id'])['name'] == 'cloning_vm' } + end + + tests("Standard Clone setting ram and cpu | The return value should") do + servers_size = compute.servers.size + response = compute.vm_clone('datacenter' => datacenter, 'template_path' => template, 'name' => 'cloning_vm', 'memoryMB' => '8192', 'numCPUs' => '8', 'wait' => true) + test("be a kind of Hash") { response.kind_of? Hash } + %w{ vm_ref new_vm task_ref }.each do |key| + test("have a #{key} key") { response.key? key } + end + test("creates a new server") { compute.servers.size == servers_size+1 } + test("new server name is set") { compute.get_virtual_machine(response['new_vm']['id'])['name'] == 'cloning_vm' } + end + + tests("Linked Clone | The return value should") do + servers_size = compute.servers.size + response = compute.vm_clone('datacenter' => datacenter, 'template_path' => template, 'name' => 'cloning_vm_linked', 'wait' => 1, 'linked_clone' => true) + test("be a kind of Hash") { response.kind_of? Hash } + %w{ vm_ref new_vm task_ref }.each do |key| + test("have a #{key} key") { response.key? key } + end + test("creates a new server") { compute.servers.size == servers_size+1 } + test("new server name is set") { compute.get_virtual_machine(response['new_vm']['id'])['name'] == 'cloning_vm_linked' } + end + + tests("When invalid input is presented") do + raises(ArgumentError, 'it should raise ArgumentError') { compute.vm_clone(:foo => 1) } + raises(Fog::Compute::Vsphere::NotFound, 'it should raise Fog::Compute::Vsphere::NotFound when the UUID is not a string') do + pending # require 'guid' + compute.vm_clone('instance_uuid' => Guid.from_s(template), 'name' => 'jefftestfoo') + end + end +end diff --git a/tests/requests/compute/vm_config_vnc_tests.rb b/tests/requests/compute/vm_config_vnc_tests.rb new file mode 100644 index 00000000..cb126cd1 --- /dev/null +++ b/tests/requests/compute/vm_config_vnc_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_config_vnc request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + reconfig_target = '50137835-88a1-436e-768e-9b2677076e67' + vnc_spec = {:port => '5900', :password => 'ssaaa', :enabled => 'true'} + + tests('The response should') do + response = compute.vm_config_vnc('instance_uuid' => reconfig_target).merge(vnc_spec) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + end + + tests('VNC attrs response should') do + response = compute.vm_get_vnc(reconfig_target) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a port key') { response.key? :port } + end +end diff --git a/tests/requests/compute/vm_destroy_tests.rb b/tests/requests/compute/vm_destroy_tests.rb new file mode 100644 index 00000000..1b4a5d66 --- /dev/null +++ b/tests/requests/compute/vm_destroy_tests.rb @@ -0,0 +1,17 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_destroy request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + booted_vm = '5032c8a5-9c5e-ba7a-3804-832a03e16381' + + tests('The response should') do + response = compute.vm_destroy('instance_uuid' => booted_vm) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + + end + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_destroy } + end + +end diff --git a/tests/requests/compute/vm_migrate_tests.rb b/tests/requests/compute/vm_migrate_tests.rb new file mode 100644 index 00000000..74f425fc --- /dev/null +++ b/tests/requests/compute/vm_migrate_tests.rb @@ -0,0 +1,16 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_migrate request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + powered_on_vm = '50137835-88a1-436e-768e-9b2677076e67' + + tests('The response should') do + response = compute.vm_migrate('instance_uuid' => powered_on_vm) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_migrate } + end +end diff --git a/tests/requests/compute/vm_power_off_tests.rb b/tests/requests/compute/vm_power_off_tests.rb new file mode 100644 index 00000000..f961bb3a --- /dev/null +++ b/tests/requests/compute/vm_power_off_tests.rb @@ -0,0 +1,26 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_power_off request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + powered_on_vm = '5032c8a5-9c5e-ba7a-3804-832a03e16381' + + tests('The response should') do + response = compute.vm_power_off('instance_uuid' => powered_on_vm) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + test('should have a power_off_type key') { response.key? 'power_off_type' } + end + + # When forcing the shutdown, we expect the result to be + { true => 'cut_power', false => 'shutdown_guest'}.each do |force, expected| + tests("When 'force' => #{force}") do + response = compute.vm_power_off('instance_uuid' => powered_on_vm, 'force' => force) + test('should retur power_off_type of #{expected}') { response['power_off_type'] == expected } + end + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_power_off } + end + +end diff --git a/tests/requests/compute/vm_power_on_tests.rb b/tests/requests/compute/vm_power_on_tests.rb new file mode 100644 index 00000000..1ee007fe --- /dev/null +++ b/tests/requests/compute/vm_power_on_tests.rb @@ -0,0 +1,17 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_power_on request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + powered_off_vm = nil + + tests('The response should') do + response = compute.vm_power_on('instance_uuid' => powered_off_vm) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_power_on } + end + +end diff --git a/tests/requests/compute/vm_reboot_tests.rb b/tests/requests/compute/vm_reboot_tests.rb new file mode 100644 index 00000000..24189183 --- /dev/null +++ b/tests/requests/compute/vm_reboot_tests.rb @@ -0,0 +1,26 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_reboot request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + powered_on_vm = '5032c8a5-9c5e-ba7a-3804-832a03e16381' + + tests('The response should') do + response = compute.vm_reboot('instance_uuid' => powered_on_vm) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + test('should have a reboot_type key') { response.key? 'reboot_type' } + end + + # When forcing the shutdown, we expect the result to be + { true => 'reset_power', false => 'reboot_guest'}.each do |force, expected| + tests("When force => #{force}") do + response = compute.vm_reboot('instance_uuid' => powered_on_vm, 'force' => force) + test("should return reboot_type of #{expected}") { response['reboot_type'] == expected } + end + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_reboot } + end + +end diff --git a/tests/requests/compute/vm_reconfig_cpus_tests.rb b/tests/requests/compute/vm_reconfig_cpus_tests.rb new file mode 100644 index 00000000..e0eeadb6 --- /dev/null +++ b/tests/requests/compute/vm_reconfig_cpus_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_reconfig_cpus request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + reconfig_target = '50137835-88a1-436e-768e-9b2677076e67' + reconfig_spec = 2 + + tests('The response should') do + response = compute.vm_reconfig_cpus('instance_uuid' => reconfig_target, 'cpus' => reconfig_spec) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_reconfig_cpus('cpus' => reconfig_spec) } + raises(ArgumentError, 'raises ArgumentError when cpus option is missing') { compute.vm_reconfig_cpus('instance_uuid' => reconfig_target) } + end + +end diff --git a/tests/requests/compute/vm_reconfig_hardware_tests.rb b/tests/requests/compute/vm_reconfig_hardware_tests.rb new file mode 100644 index 00000000..db73b3e5 --- /dev/null +++ b/tests/requests/compute/vm_reconfig_hardware_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_reconfig_hardware request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + reconfig_target = '50137835-88a1-436e-768e-9b2677076e67' + reconfig_spec = {'guestId' => 'rhel5_64Guest'} + + tests('The response should') do + response = compute.vm_reconfig_hardware('instance_uuid' => reconfig_target, 'hardware_spec' => reconfig_spec) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_reconfig_hardware('hardware_spec' => reconfig_spec) } + raises(ArgumentError, 'raises ArgumentError when hardware_spec option is missing') { compute.vm_reconfig_hardware('instance_uuid' => reconfig_target) } + end + +end diff --git a/tests/requests/compute/vm_reconfig_memory_tests.rb b/tests/requests/compute/vm_reconfig_memory_tests.rb new file mode 100644 index 00000000..64aa2694 --- /dev/null +++ b/tests/requests/compute/vm_reconfig_memory_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::Compute[:vsphere] | vm_reconfig_memory request', ['vsphere']) do + + compute = Fog::Compute[:vsphere] + + reconfig_target = '50137835-88a1-436e-768e-9b2677076e67' + reconfig_spec = 4096 + + tests('The response should') do + response = compute.vm_reconfig_memory('instance_uuid' => reconfig_target, 'memory' => reconfig_spec) + test('be a kind of Hash') { response.kind_of? Hash } + test('should have a task_state key') { response.key? 'task_state' } + end + + tests('The expected options') do + raises(ArgumentError, 'raises ArgumentError when instance_uuid option is missing') { compute.vm_reconfig_memory('memory' => reconfig_spec) } + raises(ArgumentError, 'raises ArgumentError when memory option is missing') { compute.vm_reconfig_memory('instance_uuid' => reconfig_target) } + end + +end