diff --git a/.ruby-version b/.ruby-version index 5859406..005119b 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.2.3 +2.4.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index db5624e..d42daa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Change Log +## [v0.7.2](https://github.com/compozed/ops_manager_cli/tree/v0.7.2) (2017-11-03) +[Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.7.1...v0.7.2) + +## [v0.7.1](https://github.com/compozed/ops_manager_cli/tree/v0.7.1) (2017-10-25) +[Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.7.0...v0.7.1) + +## [v0.7.0](https://github.com/compozed/ops_manager_cli/tree/v0.7.0) (2017-10-20) +[Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.5.4...v0.7.0) + +**Closed issues:** + +- OpsMAnagerCli should support new errands endpoint in versions \>= 1.10 [\#32](https://github.com/compozed/ops_manager_cli/issues/32) + +**Merged pull requests:** + +- Added support for ops\_manager\_cli to deploy via AWS [\#34](https://github.com/compozed/ops_manager_cli/pull/34) ([geofffranks](https://github.com/geofffranks)) +- Add pending-changes CLI command [\#26](https://github.com/compozed/ops_manager_cli/pull/26) ([RMeharg](https://github.com/RMeharg)) + +## [v0.5.4](https://github.com/compozed/ops_manager_cli/tree/v0.5.4) (2017-06-27) +[Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.5.3...v0.5.4) + +## [v0.5.3](https://github.com/compozed/ops_manager_cli/tree/v0.5.3) (2017-06-27) +[Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.5.2...v0.5.3) + +## [v0.5.2](https://github.com/compozed/ops_manager_cli/tree/v0.5.2) (2017-06-27) +[Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.5.1...v0.5.2) + +**Closed issues:** + +- CLI should check that UAA is available after Ops Manager upgrades [\#29](https://github.com/compozed/ops_manager_cli/issues/29) +- When upgrading opsman the token does not reset [\#27](https://github.com/compozed/ops_manager_cli/issues/27) +- Support refresh tokens for re-authentication with UAA [\#24](https://github.com/compozed/ops_manager_cli/issues/24) +- Allow toggling ops\_manager.log [\#21](https://github.com/compozed/ops_manager_cli/issues/21) + ## [v0.5.1](https://github.com/compozed/ops_manager_cli/tree/v0.5.1) (2017-01-25) [Full Changelog](https://github.com/compozed/ops_manager_cli/compare/v0.5.0...v0.5.1) diff --git a/Dockerfile b/Dockerfile index 6ecafcc..d000cca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ -FROM ruby:2.3.0 +FROM ruby:2.4.1 ENV GEM_NAME ops_manager_cli -ENV GEM_VERSION 0.5.2 +ENV GEM_VERSION 0.7.5 ENV OVFTOOL_VERSION 4.1.0-2459827 ENV OVFTOOL_INSTALLER VMware-ovftool-${OVFTOOL_VERSION}-lin.x86_64.bundle ARG DOWNLOAD_URL +# ================== Installs sshpass =============== +RUN echo "deb http://httpredir.debian.org/debian jessie utils" >> sources.list +RUN apt-get update +RUN apt-get install -y sshpass unzip + # ================== Installs OVF tools ============== RUN echo $DOWNLOAD_URL RUN wget -v ${DOWNLOAD_URL} \ @@ -13,10 +18,9 @@ RUN wget -v ${DOWNLOAD_URL} \ && rm -f ${OVFTOOL_INSTALLER}* # ================== Installs Spruce ============== -RUN wget -v --no-check-certificate https://github.com/geofffranks/spruce/releases/download/v1.0.1/spruce_1.0.1_linux_amd64.tar.gz \ - && tar -xvf spruce_1.0.1_linux_amd64.tar.gz \ - && chmod +x /spruce_1.0.1_linux_amd64/spruce \ - && ln -s /spruce_1.0.1_linux_amd64/spruce /usr/bin/. +RUN wget -v --no-check-certificate https://github.com/geofffranks/spruce/releases/download/v1.13.1/spruce-linux-amd64 \ + && chmod +x spruce-linux-amd64 \ + && ln -s /spruce-linux-amd64 /usr/bin/spruce # ================== Installs JQ ============== RUN wget -v -O /usr/local/bin/jq --no-check-certificate https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 @@ -24,6 +28,4 @@ RUN chmod +x /usr/local/bin/jq # ================== Installs ops_manager_cli gem ============== COPY pkg/${GEM_NAME}-${GEM_VERSION}.gem /tmp/ - RUN gem install /tmp/${GEM_NAME}-${GEM_VERSION}.gem - diff --git a/lib/ops_manager.rb b/lib/ops_manager.rb index 612c48c..9944a34 100644 --- a/lib/ops_manager.rb +++ b/lib/ops_manager.rb @@ -83,9 +83,10 @@ def password require "ops_manager/version" require "ops_manager/semver" -require "ops_manager/deployments/vsphere" -require 'ops_manager/configs/product_deployment' -require 'ops_manager/configs/opsman_deployment' +require "ops_manager/appliance/vsphere" +require "ops_manager/appliance/aws" +require 'ops_manager/config/product_deployment' +require 'ops_manager/config/opsman_deployment' require "ops_manager/cli" require "ops_manager/errors" require "net/https" diff --git a/lib/ops_manager/api/base.rb b/lib/ops_manager/api/base.rb index a547555..effad86 100644 --- a/lib/ops_manager/api/base.rb +++ b/lib/ops_manager/api/base.rb @@ -153,7 +153,10 @@ def http_for(uri) Net::HTTP.new(uri.host, uri.port).tap do |http| http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http.read_timeout = 1200 + http.read_timeout = 1800 + + ctx = OpenSSL::SSL::SSLContext.new + ctx.ssl_version = :TLSv1_2 end end diff --git a/lib/ops_manager/api/opsman.rb b/lib/ops_manager/api/opsman.rb index 4d62327..5f15c6e 100644 --- a/lib/ops_manager/api/opsman.rb +++ b/lib/ops_manager/api/opsman.rb @@ -25,6 +25,10 @@ def get_staged_products(opts = {}) authenticated_get("/api/v0/staged/products", opts) end + def get_pending_changes(opts = {}) + authenticated_get("/api/v0/staged/pending_changes", opts) + end + def get_installation_settings(opts = {}) print_green '====> Downloading installation settings ...' res = authenticated_get("/api/installation_settings", opts) @@ -43,7 +47,7 @@ def upload_installation_assets def get_installation_assets opts = { write_to: "installation_assets.zip" } - say_green '====> Download installation assets ...' + print_green '====> Download installation assets ...' res = authenticated_get("/api/v0/installation_asset_collection", opts) say_green 'done' res @@ -103,12 +107,16 @@ def upgrade_product_installation(guid, product_version) end def upload_product(filepath) - file = "#{filepath}" - cmd = "curl -s -k \"https://#{target}/api/v0/available_products\" -F 'product[file]=@#{file}' -X POST -H 'Authorization: Bearer #{access_token}'" - logger.info "running cmd: #{cmd}" - body = `#{cmd}` - logger.info "Upload product response: #{body}" - raise OpsManager::ProductUploadError if body.include? "error" + return unless filepath + tar = UploadIO.new(filepath, 'multipart/form-data') + print_green "====> Uploading product: #{filepath} ..." + #print "====> Uploading product ...".green + opts = { "product[file]" => tar } + res = authenticated_multipart_post("/api/v0/available_products" , opts) + + raise OpsManager::ProductUploadError.new(res.body) unless res.code == '200' + say_green 'done' + res end def get_available_products @@ -117,7 +125,7 @@ def get_available_products def get_diagnostic_report authenticated_get("/api/v0/diagnostic_report") - rescue Errno::ETIMEDOUT , Errno::EHOSTUNREACH, Net::HTTPFatalError, Net::OpenTimeout, CF::UAA::BadTarget, SocketError + rescue Errno::ETIMEDOUT , Errno::EHOSTUNREACH, Net::HTTPFatalError, Net::OpenTimeout, HTTPClient::ConnectTimeoutError, CF::UAA::BadTarget, SocketError nil end @@ -153,6 +161,35 @@ def import_stemcell(filepath) res end + def get_ensure_availability + get("/login/ensure_availability") + end + + def get_token + token_issuer.owner_password_grant(username, password, 'opsman.admin').tap do |token| + logger.info "UAA Token: #{token.inspect}" + end + rescue CF::UAA::TargetError + nil + end + + def pending_changes(opts = {}) + print_green '====> Getting pending changes ...' + res = authenticated_get('/api/v0/staged/pending_changes') + pendingChanges = JSON.parse(res.body) + + if pendingChanges['product_changes'].count == 0 + puts "\nNo pending changes" + else + pendingChanges['product_changes'].each do |product| + puts "\n#{product['guid']}" + end + end + + say_green 'done' + res + end + def username @username ||= OpsManager.get_conf(:username) end @@ -165,26 +202,36 @@ def target @target = OpsManager.get_conf(:target) end - def get_token - token_issuer.owner_password_grant(username, password, 'opsman.admin').tap do |token| - logger.info "UAA Token: #{token.inspect}" - end - rescue CF::UAA::TargetError, CF::UAA::BadTarget - nil + def reset_access_token + @access_token = nil + end + + def access_token + @access_token ||= get_token.info['access_token'] end + def wait_for_https_alive(limit) + @retry_counter = 0 + res = nil + until(@retry_counter >= limit or (res = check_alive).code.to_i < 400) do + sleep 1 + @retry_counter += 1 + end + res + end private + def check_alive + get("/") + rescue Net::OpenTimeout, Net::HTTPError, Net::HTTPFatalError, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::ECONNRESET => e + Net::HTTPInternalServerError.new(1.0, 500, e.inspect) + end + def token_issuer @token_issuer ||= CF::UAA::TokenIssuer.new( "https://#{target}/uaa", 'opsman', nil, skip_ssl_validation: true ) end - def access_token - token = get_token - @access_token = token ? token.info['access_token'] : nil - end - def authorization_header "Bearer #{access_token}" end diff --git a/lib/ops_manager/appliance/aws.rb b/lib/ops_manager/appliance/aws.rb new file mode 100644 index 0000000..15d1f2c --- /dev/null +++ b/lib/ops_manager/appliance/aws.rb @@ -0,0 +1,89 @@ +require 'fog/aws' +require 'ops_manager/appliance/base' + +class OpsManager + module Appliance + class AWS < Base + + def deploy_vm + image_id = ::YAML.load_file(ami_mapping_file)[config[:opts][:region]] + + server = connection.servers.create( + block_device_mapping: [{ + 'DeviceName' => '/dev/xvda', + 'Ebs.VolumeSize' => config[:opts][:disk_size_in_gb], + }], + key_name: config[:opts][:ssh_keypair_name], + flavor_id: config[:opts][:instance_type], + subnet_id: config[:opts][:subnet_id], + image_id: image_id, + private_ip_address: config[:ip], + security_group_ids: security_group_ids, + availability_zone: config[:opts][:availability_zone], + iam_instance_profile_name: config[:opts][:instance_profile_name], + tags: { + 'Name' => vm_name, + } + ) + server.wait_for { ready? } + return server + end + + def stop_current_vm(name) + server = connection.servers.all("private-ip-address" => config[:ip], "tag:Name" => name).first + if ! server + fail "VM not found matching IP '#{config[:ip]}', named '#{name}'" + end + server.stop + server.wait_for { server.state == "stopped" } + + # Create ami of stopped server + response = connection.create_image(server.id, "#{name}-backup", "Backup of #{name}") + image = connection.images.get( response.data[:body]['imageId']) + image.wait_for 36000 { image.state == "available" } + if image.state != "available" + fail "Error creating backup AMI, bailing out before destroying the VM" + end + + puts "Saved #{name} to AMI #{image.id} (#{name}-backup) for safe-keeping" + + server.destroy + if !Fog.mocking? + server.wait_for { server.state == 'terminated' } + else + # Fog's mock doesn't support transitioning state from terminating -> terminated + # so we have to hack this here + server.wait_for { server.state == 'terminating' } + end + end + + private + def ami_mapping_file + Dir.glob(config[:opts][:ami_mapping_file]).first + end + + def security_group_ids + config[:opts][:security_groups].collect do |sg| + connection.security_groups.get(sg).group_id + end + end + + def connection + if config[:opts][:use_iam_profile] + @connection ||= Fog::Compute.new({ + provider: config[:provider], + use_iam_profile: config[:opts][:use_iam_profile], + aws_access_key_id: "", + aws_secret_access_key: "", + }) + else + @connection ||= Fog::Compute.new({ + provider: config[:provider], + aws_access_key_id: config[:opts][:access_key], + aws_secret_access_key: config[:opts][:secret_key], + }) + end + end + end + end +end diff --git a/lib/ops_manager/appliance/base.rb b/lib/ops_manager/appliance/base.rb new file mode 100644 index 0000000..0d6bbe2 --- /dev/null +++ b/lib/ops_manager/appliance/base.rb @@ -0,0 +1,23 @@ + +class OpsManager + module Appliance + class Base + attr_reader :config + + def initialize(config) + @config = config + end + def deploy_vm + raise NotImplementedError.new("You must implement deploy_vm.") + end + def stop_current_vm(name) + raise NotImplementedError.new("You must implement stop_current_vm.") + end + + private + def vm_name + @vm_name ||= "#{config[:name]}-#{config[:desired_version]}" + end + end + end +end diff --git a/lib/ops_manager/appliance/vsphere.rb b/lib/ops_manager/appliance/vsphere.rb new file mode 100644 index 0000000..32f388d --- /dev/null +++ b/lib/ops_manager/appliance/vsphere.rb @@ -0,0 +1,47 @@ +require 'rbvmomi' +require "uri" +require 'shellwords' +require "ops_manager/logging" +require 'ops_manager/appliance/base' + +class OpsManager + module Appliance + class Vsphere < Base + include OpsManager::Logging + attr_reader :config + + def deploy_vm + print '====> Deploying ova ...'.green + vcenter_target= "vi://#{vcenter_username}:#{vcenter_password}@#{config[:opts][:vcenter][:host]}/#{config[:opts][:vcenter][:datacenter]}/host/#{config[:opts][:vcenter][:cluster]}" + cmd = "echo yes | ovftool --acceptAllEulas --noSSLVerify --powerOn --X:waitForIp --net:\"Network 1=#{config[:opts][:portgroup]}\" --name=#{vm_name} -ds=#{config[:opts][:datastore]} --prop:ip0=#{config[:ip]} --prop:netmask0=#{config[:opts][:netmask]} --prop:gateway=#{config[:opts][:gateway]} --prop:DNS=#{config[:opts][:dns]} --prop:ntp_servers=#{config[:opts][:ntp_servers].join(',')} --prop:admin_password=#{config[:password]} #{config[:opts][:ova_path]} #{vcenter_target}" + logger.info "Running: #{cmd}" + logger.info `#{cmd}` + puts 'done'.green + end + + def stop_current_vm(name) + print "====> Stopping vm #{name} ...".green + dc = vim.serviceInstance.find_datacenter(config[:opts][:vcenter][:datacenter]) + logger.info "finding vm: #{name}" + vm = dc.find_vm(name) or fail "VM not found" + vm.PowerOffVM_Task.wait_for_completion + puts 'done'.green + end + + private + def vim + RbVmomi::VIM.connect host: config[:opts][:vcenter][:host], user: URI.unescape(config[:opts][:vcenter][:username]), password: URI.unescape(config[:opts][:vcenter][:password]), insecure: true + end + + def vcenter_username + Shellwords.escape(URI.encode(config[:opts][:vcenter][:username])) + end + + def vcenter_password + Shellwords.escape(URI.encode(config[:opts][:vcenter][:password])) + end + end + end +end + + diff --git a/lib/ops_manager/appliance_deployment.rb b/lib/ops_manager/appliance_deployment.rb index d1e5d52..5efe2a6 100644 --- a/lib/ops_manager/appliance_deployment.rb +++ b/lib/ops_manager/appliance_deployment.rb @@ -1,15 +1,18 @@ require "ops_manager/api/opsman" require "ops_manager/api/pivnet" -require 'ops_manager/configs/opsman_deployment' +require 'ops_manager/config/opsman_deployment' require 'fileutils' class OpsManager::ApplianceDeployment extend Forwardable + attr_reader :config + def_delegators :pivnet_api, :get_product_releases, :accept_product_release_eula, :get_product_release_files, :download_product_release_file - def_delegators :opsman_api, :create_user, :trigger_installation, :get_installation_assets, - :get_installation_settings, :get_diagnostic_report, :upload_installation_assets, - :import_stemcell, :target, :password, :username, :ops_manager_version= + def_delegators :opsman_api, :create_user, :get_installation_assets, + :get_installation_settings, :get_diagnostic_report, :upload_installation_assets, :get_ensure_availability, + :import_stemcell, :target, :password, :username, :ops_manager_version= , :reset_access_token, :get_pending_changes, + :wait_for_https_alive attr_reader :config_file @@ -18,40 +21,45 @@ def initialize(config_file) end def run - OpsManager.set_conf(:target, config.ip) - OpsManager.set_conf(:username, config.username) - OpsManager.set_conf(:password, config.password) - OpsManager.set_conf(:pivnet_token, config.pivnet_token) + OpsManager.set_conf(:target, config[:ip]) + OpsManager.set_conf(:username, config[:username]) + OpsManager.set_conf(:password, config[:password]) + OpsManager.set_conf(:pivnet_token, config[:pivnet_token]) self.extend(OpsManager::Deployments::Vsphere) - if config.has_key?('hostname') then - OpsManager.set_conf(:target, config.hostname) - end + + if config.has_key?('hostname') then + OpsManager.set_conf(:target, config.hostname) + end + case when current_version.empty? - puts "No OpsManager deployed at #{config.ip}. Deploying ...".green + puts "No OpsManager deployed at #{config[:ip]}. Deploying ...".green deploy if config.has_key?('hostname') then OpsManager.set_conf(:target, config.hostname) end create_first_user when current_version < desired_version then - puts "OpsManager at #{config.ip} version is #{current_version}. Upgrading to #{desired_version} .../".green + puts "OpsManager at #{config[:ip]} version is #{current_version}. Upgrading to #{desired_version} .../".green upgrade when current_version == desired_version then - puts "OpsManager at #{config.ip} version is already #{config.desired_version}. Skiping ...".green + if pending_changes? + puts "OpsManager at #{config[:ip]} version has pending changes. Applying changes...".green + OpsManager::InstallationRunner.trigger!.wait_for_result + else + puts "OpsManager at #{config[:ip]} version is already #{config[:desired_version]}. Skiping ...".green + end end puts '====> Finish!'.green end - def deploy - deploy_vm(desired_vm_name , config.ip) - end - - %w{ stop_current_vm deploy_vm }.each do |m| - define_method(m) do - raise NotImplementedError + def appliance + @appliance ||= if config[:provider] =~/vsphere/i + OpsManager::Appliance::Vsphere.new(config) + else + OpsManager::Appliance::AWS.new(config) end end @@ -62,20 +70,30 @@ def create_first_user end end + def deploy + appliance.deploy_vm + wait_for_https_alive 300 + end + def upgrade get_installation_assets download_current_stemcells - stop_current_vm(current_vm_name) + appliance.stop_current_vm(current_name) deploy upload_installation_assets + wait_for_uaa provision_stemcells OpsManager::InstallationRunner.trigger!.wait_for_result end def list_current_stemcells JSON.parse(installation_settings).fetch('products').inject([]) do |a, p| - a << p['stemcell'].fetch('version') - end + product_name = "stemcells" + if p['stemcell'].fetch('os') =~ /windows/i + product_name = "stemcells-windows-server" + end + a << { version: p['stemcell'].fetch('version'), product: product_name } + end.uniq end # Finds available stemcell's pivotal network release. @@ -83,9 +101,9 @@ def list_current_stemcells # # # @param version [String] the version number, eg: '2362.17' # @return release_id [Integer] the pivotal netowkr release id of the found stemcell. - def find_stemcell_release(version) + def find_stemcell_release(version, product_name) version = OpsManager::Semver.new(version) - releases = stemcell_releases.collect do |r| + releases = stemcell_releases(product_name).collect do |r| { release_id: r['id'], version: OpsManager::Semver.new(r['version']), @@ -103,8 +121,8 @@ def find_stemcell_release(version) # @param release_id [String] the version number, eg: '2362.17' # @param filename [Regex] the version number, eg: /vsphere/ # @return id and name [Array] the pivotal network file ID and Filename for the matching stemcell. - def find_stemcell_file(release_id, filename) - files = JSON.parse(get_product_release_files('stemcells', release_id).body).fetch('product_files') + def find_stemcell_file(release_id, filename, product_name) + files = JSON.parse(get_product_release_files(product_name, release_id).body).fetch('product_files') file = files.select{ |r| r.fetch('aws_object_key') =~ filename }.first return file['id'], file['aws_object_key'].split('/')[-1] end @@ -112,35 +130,60 @@ def find_stemcell_file(release_id, filename) # Lists all the available stemcells in the current installation_settings. # Downloads those stemcells. def download_current_stemcells - puts "Downloading existing stemcells ...".green + print "====> Downloading existing stemcells ...".green + puts "no stemcells found".green if list_current_stemcells.empty? FileUtils.mkdir_p current_stemcell_dir - list_current_stemcells.uniq.each do |stemcell_version| - release_id = find_stemcell_release(stemcell_version) - accept_product_release_eula('stemcells', release_id ) - file_id, file_name = find_stemcell_file(release_id, /vsphere/) - download_product_release_file('stemcells', release_id, file_id, write_to: "#{current_stemcell_dir}/#{file_name}") + list_current_stemcells.uniq.each do |stemcell_info| + stemcell_version = stemcell_info[:version] + product_name = stemcell_info[:product] + release_id = find_stemcell_release(stemcell_version, product_name) + accept_product_release_eula(product_name, release_id) + stemcell_regex = /vsphere/ + if config[:provider] == "AWS" + stemcell_regex = /aws/ + end + + file_id, file_name = find_stemcell_file(release_id, stemcell_regex, product_name) + download_product_release_file(product_name, release_id, file_id, write_to: "#{current_stemcell_dir}/#{file_name}") end end def new_vm_name - @new_vm_name ||= "#{config.name}-#{config.desired_version}" + @new_vm_name ||= "#{config[:name]}-#{config[:desired_version]}" end def current_version @current_version ||= OpsManager::Semver.new(version_from_diagnostic_report) end + def current_name + @current_name ||= "#{config[:name]}-#{current_version}" + end + def desired_version - @desired_version ||= OpsManager::Semver.new(config.desired_version) + @desired_version ||= OpsManager::Semver.new(config[:desired_version]) end def provision_stemcells + reset_access_token Dir.glob("#{current_stemcell_dir}/*").each do |stemcell_filepath| import_stemcell(stemcell_filepath) end end + def wait_for_uaa + puts '====> Waiting for UAA to become available ...'.green + while !uaa_available? + sleep(5) + end + end + private + def uaa_available? + res = get_ensure_availability + res.code.eql? '302' and res.body.include? '/auth/cloudfoundry' + end + def diagnostic_report @diagnostic_report ||= get_diagnostic_report end @@ -158,12 +201,9 @@ def parsed_diagnostic_report end def current_vm_name - @current_vm_name ||= "#{config.name}-#{current_version}" + @current_vm_name ||= "#{config[:name]}-#{current_version}" end - def desired_vm_name - @desired_vm_name ||= "#{config.name}-#{config.desired_version}" - end def pivnet_api @pivnet_api ||= OpsManager::Api::Pivnet.new @@ -174,10 +214,10 @@ def opsman_api end def config - parsed_yml = ::YAML.load_file(@config_file) - @config ||= OpsManager::Configs::OpsmanDeployment.new(parsed_yml) + @config ||= OpsManager::Config::OpsmanDeployment.new(YAML.load_file(@config_file)) end + def desired_version?(version) !!(desired_version.to_s =~/#{version}/) end @@ -186,15 +226,15 @@ def installation_settings @installation_settings ||= get_installation_settings.body end - def get_stemcell_releases - get_product_releases('stemcells') - end - - def stemcell_releases - @stemcell_releases ||= JSON.parse(get_stemcell_releases.body).fetch('releases') + def stemcell_releases(product_name) + JSON.parse(get_product_releases(product_name).body).fetch('releases') end def current_stemcell_dir "/tmp/current_stemcells" end + + def pending_changes? + !JSON.parse(get_pending_changes.body).fetch('product_changes').empty? + end end diff --git a/lib/ops_manager/cli.rb b/lib/ops_manager/cli.rb index a71b3c2..9bbaed6 100644 --- a/lib/ops_manager/cli.rb +++ b/lib/ops_manager/cli.rb @@ -72,6 +72,12 @@ def execute end end + class PendingChanges < Clamp::Command + def execute + OpsManager::Api::Opsman.new.pending_changes + end + end + class DeleteUnusedProducts < Clamp::Command def execute @@ -158,6 +164,7 @@ def execute subcommand "get-installation-settings", "Gets installation settings", GetInstallationSettings subcommand "get-installation-logs", "Gets installation logs", GetInstallationLogs subcommand "import-stemcell", "Uploads stemcell to OpsManager", ImportStemcell + subcommand "pending-changes", "View pending changes in OpsManager", PendingChanges end end diff --git a/lib/ops_manager/config/base.rb b/lib/ops_manager/config/base.rb new file mode 100644 index 0000000..cccd919 --- /dev/null +++ b/lib/ops_manager/config/base.rb @@ -0,0 +1,43 @@ +require 'ostruct' + +class OpsManager + module Config + class Base < OpenStruct + def initialize(config) + super(config.to_symbolize) + end + + def validate_presence_of!(*attrs) + attrs.each do |attr| + raise "missing #{attr} on config" unless self.to_h.has_key?(attr) + end + end + + def expand_path_for!(*attrs) + attrs.each do |attr| + path = self[attr] + next if path.nil? + self[attr] = if path =~ %r{^file://} + path = Dir.glob(path.gsub!('file://','')).first + "file://#{path}" + else + Dir.glob(path).first + end + end + end + end + end +end + +class Hash + def to_symbolize + Hash[self.map do |k, v| + if v.kind_of?(Hash) + [k.to_sym, v.to_symbolize] + else + [k.to_sym, v] + end + end] + end +end + diff --git a/lib/ops_manager/configs/opsman_deployment.rb b/lib/ops_manager/config/opsman_deployment.rb similarity index 82% rename from lib/ops_manager/configs/opsman_deployment.rb rename to lib/ops_manager/config/opsman_deployment.rb index da13d15..ba189b4 100644 --- a/lib/ops_manager/configs/opsman_deployment.rb +++ b/lib/ops_manager/config/opsman_deployment.rb @@ -1,7 +1,7 @@ -require 'ops_manager/configs/base' +require 'ops_manager/config/base' class OpsManager - class Configs + module Config class OpsmanDeployment < Base def initialize(config) super(config) diff --git a/lib/ops_manager/config/product_deployment.rb b/lib/ops_manager/config/product_deployment.rb new file mode 100644 index 0000000..c8e600f --- /dev/null +++ b/lib/ops_manager/config/product_deployment.rb @@ -0,0 +1,13 @@ +require 'ops_manager/config/base' + +class OpsManager + module Config + class ProductDeployment < Base + def initialize(config) + super(config) + validate_presence_of! :name, :desired_version + expand_path_for! :stemcell, :filepath + end + end + end +end diff --git a/lib/ops_manager/configs/base.rb b/lib/ops_manager/configs/base.rb index 2f4e604..4c0bb22 100644 --- a/lib/ops_manager/configs/base.rb +++ b/lib/ops_manager/configs/base.rb @@ -2,7 +2,7 @@ class OpsManager class Configs - class Base < OpenStruct + class Base < Hash def initialize(config) @config = config super(config) diff --git a/lib/ops_manager/configs/product_deployment.rb b/lib/ops_manager/configs/product_deployment.rb deleted file mode 100644 index 549b6e4..0000000 --- a/lib/ops_manager/configs/product_deployment.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'ops_manager/configs/base' - -class OpsManager - class Configs - class ProductDeployment < Base - def initialize(config) - super(config) - validate_presence_of!(:name, :desired_version) - end - - def stemcell - find_full_path(@config['stemcell']) - end - end - end -end diff --git a/lib/ops_manager/deployments/vsphere.rb b/lib/ops_manager/deployments/vsphere.rb deleted file mode 100644 index 1f2fd30..0000000 --- a/lib/ops_manager/deployments/vsphere.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'rbvmomi' -require "uri" -require 'shellwords' -require "ops_manager/logging" - -class OpsManager - module Deployments - module Vsphere - include OpsManager::Logging - - def deploy_vm(name, ip) - print '====> Deploying ova ...'.green - vcenter_target= "vi://#{vcenter_username}:#{vcenter_password}@#{config.opts['vcenter']['host']}/#{config.opts['vcenter']['datacenter']}/host/#{config.opts['vcenter']['cluster']}" - cmd = "echo yes | ovftool --acceptAllEulas --noSSLVerify --powerOn --X:waitForIp --net:\"Network 1=#{config.opts['portgroup']}\" --name=#{name} -ds=#{config.opts['datastore']} --prop:ip0=#{ip} --prop:netmask0=#{config.opts['netmask']} --prop:gateway=#{config.opts['gateway']} --prop:DNS=#{config.opts['dns']} --prop:ntp_servers=#{config.opts['ntp_servers'].join(',')} --prop:admin_password=#{config.password} #{config.opts['ova_path']} #{vcenter_target}" - logger.info "Running: #{cmd}" - logger.info `#{cmd}` - puts 'done'.green - end - - def stop_current_vm(name) - print "====> Stopping vm #{name} ...".green - dc = vim.serviceInstance.find_datacenter(config.opts['vcenter']['datacenter']) - logger.info "finding vm: #{name}" - vm = dc.find_vm(name) or fail "VM not found" - vm.PowerOffVM_Task.wait_for_completion - puts 'done'.green - end - - private - def vim - RbVmomi::VIM.connect host: config.opts['vcenter']['host'], user: URI.unescape(config.opts['vcenter']['username']), password: URI.unescape(config.opts['vcenter']['password']), insecure: true - end - - def vcenter_username - Shellwords.escape(URI.encode(config.opts['vcenter']['username'])) - end - - def vcenter_password - Shellwords.escape(URI.encode(config.opts['vcenter']['password'])) - end - end - end -end - - diff --git a/lib/ops_manager/director_template_generator.rb b/lib/ops_manager/director_template_generator.rb index 00fe3c7..2a85f27 100644 --- a/lib/ops_manager/director_template_generator.rb +++ b/lib/ops_manager/director_template_generator.rb @@ -7,9 +7,8 @@ def generate installation_settings.delete(property_name) end - %w{ director_ssl uaa_ssl uaa_credentials uaa_admin_user_credentials - uaa_admin_client_credentials }.each do |property_name| - product_template["products"][1].delete(property_name) + %w{ uaa_credentials uaa_admin_user_credentials uaa_admin_client_credentials }.each do |property_name| + product_template["products"].select {|p| p["identifier"] == "p-bosh"}.first.delete(property_name) end add_merging_strategy_for_networks diff --git a/lib/ops_manager/installation_runner.rb b/lib/ops_manager/installation_runner.rb index 38626a5..cc75967 100644 --- a/lib/ops_manager/installation_runner.rb +++ b/lib/ops_manager/installation_runner.rb @@ -6,7 +6,7 @@ class InstallationRunner attr_reader :id def trigger! - res = trigger_installation( body: body ) + res = trigger_installation( :headers => {"Content-Type"=>"application/json"}, :body => body ) @id = JSON.parse(res.body).fetch('install').fetch('id').to_i self end @@ -25,27 +25,19 @@ def wait_for_result private def body - @body ||= [ 'ignore_warnings=true' ] - @body << errands_body - @body.join('&') + @body ||= {'errands' => errands, 'ignore_warnings' => true }.to_json end - def opsman_api @opsman_api ||= OpsManager::Api::Opsman.new end - def errands_body - staged_products_guids.collect { |product_guid| post_deploy_errands_body_for(product_guid) } - end - - def post_deploy_errands_body_for(product_guid) - post_deploy_errands = post_deploy_errands_for(product_guid) - - if post_deploy_errands.empty? - "enabled_errands[#{product_guid}]{}" - else - post_deploy_errands.collect{ |e| "enabled_errands[#{product_guid}][post_deploy_errands][]=#{e}" } + def errands + res = { } + staged_products_guids.each do |product_guid| + errands = errands_for(product_guid).keep_if { |an_errand| an_errand['post_deploy'] } + errands.each { |e| res[product_guid] = {'run_post_deploy' => { e['name'] => true}}} end + res end def staged_products_guids @@ -56,9 +48,6 @@ def staged_products JSON.parse(get_staged_products.body) end - def post_deploy_errands_for(product_guid) - errands_for(product_guid).keep_if{ |errand| errand['post_deploy'] }.map{ |o| o['name']} - end def errands_for(product_guid) res = get_staged_products_errands(product_guid) diff --git a/lib/ops_manager/logging.rb b/lib/ops_manager/logging.rb index ab9ce0e..53706d2 100644 --- a/lib/ops_manager/logging.rb +++ b/lib/ops_manager/logging.rb @@ -7,13 +7,11 @@ def logger end def self.logger - @logger ||= Logger.new(STDOUT).tap do |l| - l.level = log_level - end + @logger ||= Logger.new(STDERR, level: log_level) end - def self.logger=(logger) - @logger = logger + def self.logger=(l) + @logger = l end private diff --git a/lib/ops_manager/product_deployment.rb b/lib/ops_manager/product_deployment.rb index d90fa25..0e542f2 100644 --- a/lib/ops_manager/product_deployment.rb +++ b/lib/ops_manager/product_deployment.rb @@ -7,6 +7,8 @@ class OpsManager class ProductDeployment extend Forwardable + attr_reader :config + def_delegators :opsman_api, :current_version, :upload_product, :get_installation_settings, :upgrade_product_installation, :get_installation, :get_available_products, :upload_installation_settings, :trigger_installation, :import_stemcell, :add_staged_products @@ -19,12 +21,12 @@ def initialize(config_file, forced_deployment = false) end def installation - OpsManager::ProductInstallation.find(config.name) + OpsManager::ProductInstallation.find(config[:name]) end def run - OpsManager.target_and_login(config.target, config.username, config.password) - import_stemcell(config.stemcell) + OpsManager.target_and_login(config[:target], config[:username], config[:password]) + import_stemcell(config[:stemcell]) case when installation.nil? || forced_deployment? @@ -37,16 +39,14 @@ def run end def desired_version - Semver.new(config.desired_version) + Semver.new(config[:desired_version]) end def upload - print "====> Uploading product ...".green - if ProductDeployment.exists?(config.name, config.desired_version) - puts "product already exists".green - elsif config.filepath - upload_product(config.filepath) - puts "done".green + if ProductDeployment.exists?(config[:name], config[:desired_version]) + puts "====> Product already exists... skipping upload".green + elsif config[:filepath] + upload_product(config[:filepath]) else puts "no filepath provided, skipping product upload.".green end @@ -57,9 +57,9 @@ def upgrade puts "====> Skipping as this product has a pending installation!".red return end - puts "====> Upgrading #{config.name} version from #{installation.current_version.to_s} to #{config.desired_version}...".green + puts "====> Upgrading #{config[:name]} version from #{installation.current_version.to_s} to #{config[:desired_version]}...".green upload - upgrade_product_installation(installation.guid, config.desired_version) + upgrade_product_installation(installation.guid, config[:desired_version]) merge_product_installation_settings OpsManager::InstallationRunner.trigger!.wait_for_result @@ -68,12 +68,12 @@ def upgrade def add_to_installation unless installation - add_staged_products(config.name, config.desired_version) + add_staged_products(config[:name], config[:desired_version]) end end def deploy - puts "====> Deploying #{config.name} version #{config.desired_version}...".green + puts "====> Deploying #{config[:name]} version #{config[:desired_version]}...".green upload add_to_installation merge_product_installation_settings @@ -85,7 +85,7 @@ def deploy def merge_product_installation_settings get_installation_settings({write_to: '/tmp/is.yml'}) - puts `DEBUG=false spruce merge /tmp/is.yml #{config.installation_settings_file} > /tmp/new_is.yml` + puts `DEBUG=false DEFAULT_ARRAY_MERGE_KEY=identifier spruce merge /tmp/is.yml #{config[:installation_settings_file]} > /tmp/new_is.yml` upload_installation_settings('/tmp/new_is.yml') end @@ -96,7 +96,7 @@ def self.exists?(name, version) private def desired_version - @desired_version ||= OpsManager::Semver.new(config.desired_version) + @desired_version ||= OpsManager::Semver.new(config[:desired_version]) end def forced_deployment? @@ -108,7 +108,7 @@ def opsman_api end def config - OpsManager::Configs::ProductDeployment.new(::YAML.load_file(@config_file)) + OpsManager::Config::ProductDeployment.new(::YAML.load_file(@config_file)) end end end diff --git a/lib/ops_manager/product_template_generator.rb b/lib/ops_manager/product_template_generator.rb index af7fb8c..5646ee1 100644 --- a/lib/ops_manager/product_template_generator.rb +++ b/lib/ops_manager/product_template_generator.rb @@ -19,37 +19,38 @@ def generate delete_from_jobs(property_name) end - %w{ password secret salt private_key_pem }.each do |property_name| + %w{ password secret salt }.each do |property_name| delete_value_from_job_properties(property_name) end - %w{ secret private_key_pem }.each do |property_name| + %w{ secret }.each do |property_name| delete_value_from_product_properties(property_name) end - add_merging_strategy_for_jobs - add_merging_strategy_for_job_properties + %w{ deployed }.each do |property_name| + delete_key_from_product_properties(property_name) + end - { 'products' => [ "(( merge on identifier ))" , selected_product ] } - end + %w{ deployed }.each do |property_name| + delete_key_from_job_properties(property_name) + end - def generate_yml - generate.to_yaml - .gsub('"(( merge on identifier ))"', '(( merge on identifier ))') - end + %w{ deployed }.each do |property_name| + delete_key_from_product_properties_options_properties(property_name) + end - private + %w{ deployed }.each do |property_name| + delete_key_from_job_properties_records_properties(property_name) + end - def add_merging_strategy_for_jobs - selected_product['jobs'].unshift("(( merge on identifier ))") + { 'products' => [ selected_product ] } end - def add_merging_strategy_for_job_properties - selected_product['jobs'].each do |j| - j['properties'].unshift("(( merge on identifier ))") if j['properties'] - end + def generate_yml + generate.to_yaml end + private def delete_from_product(name) selected_product.delete(name) end @@ -81,6 +82,42 @@ def delete_value_from_product_properties(name) end end + def delete_key_from_job_properties(name) + selected_product['jobs'].each do |j| + j.fetch('properties', []).each do |p| + p.delete(name) + end + end + end + + def delete_key_from_product_properties(name) + selected_product.fetch('properties', []).each do |p| + p.delete(name) + end + end + + def delete_key_from_product_properties_options_properties(name) + selected_product.fetch('properties',[]).each do |p| + p.fetch('options',[]).each do |o| + o.fetch('properties', []).each do |op| + op.delete(name) + end + end + end + end + + def delete_key_from_job_properties_records_properties(name) + selected_product['jobs'].each do |j| + j.fetch('properties',[]).each do |p| + p.fetch('records',[]).each do |o| + o.fetch('properties', []).each do |op| + op.delete(name) + end + end + end + end + end + def delete_value_from_job_properties(name) selected_product['jobs'].each do |j| j.fetch('properties', []).each do |p| diff --git a/lib/ops_manager/version.rb b/lib/ops_manager/version.rb index 83ab9dc..d01d8d7 100644 --- a/lib/ops_manager/version.rb +++ b/lib/ops_manager/version.rb @@ -1,3 +1,7 @@ class OpsManager +<<<<<<< HEAD VERSION = "0.5.2" +======= + VERSION = "0.7.5" +>>>>>>> upstream/master end diff --git a/ops_manager.gemspec b/ops_manager.gemspec index 80e202c..2687997 100644 --- a/ops_manager.gemspec +++ b/ops_manager.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |spec| spec.add_dependency "rbvmomi" spec.add_dependency "multipart-post" spec.add_dependency "clamp" + spec.add_dependency "fog-aws" spec.add_dependency "net-ping" spec.add_dependency "cf-uaa-lib" spec.add_dependency "session_config" diff --git a/spec/dummy/ami/ami.yml b/spec/dummy/ami/ami.yml new file mode 100644 index 0000000..c5b314b --- /dev/null +++ b/spec/dummy/ami/ami.yml @@ -0,0 +1,16 @@ +--- +ap-south-1: ami-67541408 +eu-west-2: ami-d2d2c0b6 +eu-west-1: ami-368b5d4f +ap-northeast-2: ami-7922f817 +ap-northeast-1: ami-36a57650 +sa-east-1: ami-0faad563 +ca-central-1: ami-ef2e978b +ap-southeast-1: ami-69d9a60a +ap-southeast-2: ami-d2dc3fb0 +eu-central-1: ami-d18a37be +us-east-1: ami-a26bacd8 +us-east-2: ami-0eddf06b +us-west-1: ami-bb4a79db +us-west-2: ami-30d61348 +us-gov-west-1: ami-6a4ecc0b diff --git a/spec/dummy/example-product-1.6.1.pivotal b/spec/dummy/example-product-1.6.1.pivotal deleted file mode 100644 index b2e9989..0000000 Binary files a/spec/dummy/example-product-1.6.1.pivotal and /dev/null differ diff --git a/spec/dummy/example-product-1.6.2.pivotal b/spec/dummy/example-product-1.6.2.pivotal deleted file mode 100644 index 7b69632..0000000 Binary files a/spec/dummy/example-product-1.6.2.pivotal and /dev/null differ diff --git a/spec/dummy/ops_manager_deployment.yml b/spec/dummy/ops_manager_deployment.yml deleted file mode 100644 index 896d0d5..0000000 --- a/spec/dummy/ops_manager_deployment.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: 'ops-manager' - -provider: vsphere -desired_version: 1.4.11.0 -ip: 1.2.3.4 -username: foo -password: bar -pivnet_token: abc123 -opts: - ova_path: ops-manager.ova # you can also specify the path with *. e.g.: ops-manager-ova/*.ova. This is usefull when using concourse and the pivnet-resource - portgroup: 'dummy-portgroup' - netmask: '255.255.255.0' - gateway: '1.2.3.1' - dns: '8.8.8.8' - datastore: 'DS1' - vcenter: - username: VM_VCENTER_USER - password: VM_VCENTER_PASSWORD - host: 1.2.3.2 - datacenter: VM_DATACENTER - cluster: VM_CLUSTER - ntp_servers: - - clock1.example.com - - clock2.example.com diff --git a/spec/dummy/product_deployment.yml b/spec/dummy/product_deployment.yml index 37b012e..ef1596c 100644 --- a/spec/dummy/product_deployment.yml +++ b/spec/dummy/product_deployment.yml @@ -4,6 +4,6 @@ username: foo # Optional replacement of ./ops_manager login foo bar password: bar # Optional replacement of ./ops_manager login foo bar name: 'example-product' desired_version: '1.6.2.0' -filepath: 'example-product-1.6.2.pivotal' # you can also specify the path with *. e.g.: tile/*.pivotal. This is usefull when using concourse and the pivnet-resource +filepath: 'tile.pivotal' # you can also specify the path with *. e.g.: tile/*.pivotal. This is usefull when using concourse and the pivnet-resource stemcell: 'stemcell.tgz' # you can also specify the path with *. e.g.: stemcell/*.pivotal. This is usefull when using concourse and the pivnet-resource installation_settings_file: '../fixtures/installation_settings.json' # Generate it with ./ops_manager generate-product-template example-product diff --git a/spec/dummy/tile.pivotal b/spec/dummy/tile.pivotal new file mode 100644 index 0000000..a930238 Binary files /dev/null and b/spec/dummy/tile.pivotal differ diff --git a/spec/ops_manager/api/opsman_spec.rb b/spec/ops_manager/api/opsman_spec.rb index e179175..812ecb0 100644 --- a/spec/ops_manager/api/opsman_spec.rb +++ b/spec/ops_manager/api/opsman_spec.rb @@ -10,12 +10,13 @@ let(:filepath) { 'example-product-1.6.1.pivotal' } let(:parsed_response){ JSON.parse(response.body) } let(:token_issuer){ double } - let(:uaa_token){ double(info: {'access_token' => "UAA_ACCESS_TOKEN" }) } + let(:token){ double(info: {'access_token' => "UAA_ACCESS_TOKEN" }) } + let(:other_token){ double(info: {'access_token' => "OTHER_UAA_ACCESS_TOKEN" }) } before do allow(token_issuer).to receive(:owner_password_grant) .with(username, password, 'opsman.admin') - .and_return(uaa_token) + .and_return(token, other_token) allow(CF::UAA::TokenIssuer).to receive(:new) .with("https://#{target}/uaa", 'opsman', nil, skip_ssl_validation: true) .and_return(token_issuer) @@ -25,25 +26,61 @@ OpsManager.set_conf(:password, ENV['PASSWORD'] || password) end - describe '#upload_product' do - subject(:upload_product){ opsman.upload_product(product_filepath) } - let(:product_filepath){ 'example-product.pivotal' } + describe "#upload_product" do + subject(:upload_product){ opsman.upload_product("tile.pivotal") } - it 'performs the correct curl' do - expect(opsman).to receive(:`).with("curl -s -k \"https://#{target}/api/v0/available_products\" -F 'product[file]=@#{product_filepath}' -X POST -H 'Authorization: Bearer UAA_ACCESS_TOKEN'").and_return('{}') - upload_product + let(:response_body){ '{}' } + let(:response){ upload_product } + let(:status_code){ 200 } + let(:uri){ "#{base_uri}/api/v0/available_products" } + + before do + stub_request(:post, uri).to_return(status: status_code, body: response_body) end - describe 'when upload product errors' do - let(:body){ '{"error":"something went wrong"}' } + it "should run successfully" do + expect(response.code).to eq("200") + end - before { allow(opsman).to receive(:`).and_return(body) } + it "should include products in its body" do + expect(parsed_response).to eq({}) + end + + describe "when product is nil" do + it "should skip" do + expect(opsman).not_to receive(:puts).with(/====> Uploading product.../) + opsman.upload_product(nil) + end + end + + describe "when fails to upload product" do + let(:status_code){ 400 } + let(:response_body){ '{"error": "someting failed" }' } - it 'should raise an exception' do + it 'should raise OpsManager::ProductUploadError' do expect{ upload_product }.to raise_error{ OpsManager::ProductUploadError } end end end + # describe '#upload_product' do + # subject(:upload_product){ opsman.upload_product(product_filepath) } + # let(:product_filepath){ 'example-product.pivotal' } + + # it 'performs the correct curl' do + # expect(opsman).to receive(:`).with("curl -s -k \"https://#{target}/api/v0/available_products\" -F 'product[file]=@#{product_filepath}' -X POST -H 'Authorization: Bearer UAA_ACCESS_TOKEN'").and_return('{}') + # upload_product + # end + + # describe 'when upload product errors' do + # let(:body){ '{"error":"something went wrong"}' } + + # before { allow(opsman).to receive(:`).and_return(body) } + + # it 'should raise an exception' do + # expect{ upload_product }.to raise_error{ OpsManager::ProductUploadError } + # end + # end + # end describe '#upload_installation_assets' do before do @@ -93,6 +130,19 @@ end end + describe 'get_pending_changes' do + let(:uri){ "https://#{target}/api/v0/staged/pending_changes" } + before do + stub_request(:get, uri). + to_return(:status => 200, :body => '{}') + end + + it 'should get pending changes successfully' do + opsman.get_pending_changes + expect(WebMock).to have_requested(:get, uri) + .with(:headers => {'Authorization'=>'Bearer UAA_ACCESS_TOKEN'}) + end + end describe 'get_installation_settings' do let(:uri){ "https://#{target}/api/installation_settings" } @@ -305,7 +355,11 @@ end [ Net::OpenTimeout, Errno::ETIMEDOUT , - Net::HTTPFatalError.new( '', '' ), Errno::EHOSTUNREACH, CF::UAA::BadTarget, SocketError ].each do |error| + HTTPClient::ConnectTimeoutError, + Net::HTTPFatalError.new( '', '' ), + Errno::EHOSTUNREACH, + CF::UAA::BadTarget, + SocketError ].each do |error| describe "when there is no ops manager and request errors: #{error}" do it "should be nil" do @@ -496,4 +550,81 @@ .with(:headers => {'Authorization'=>'Bearer UAA_ACCESS_TOKEN'}) end end + + describe '#reset_token' do + it 'should reset the token' do + expect do + opsman.reset_access_token + end.to change{ opsman.access_token }.from("UAA_ACCESS_TOKEN").to("OTHER_UAA_ACCESS_TOKEN") + end + end + + describe '#get_ensure_availablity' do + let(:uri){ "https://#{target}/login/ensure_availability" } + + it 'should perform a get on /login/ensure_availability' do + stub_request(:get, uri) + + opsman.get_ensure_availability + + expect(WebMock).to have_requested(:get, uri) + end + end + + describe "#pending_changes" do + subject(:pending_changes){ opsman.pending_changes } + + let(:response_body){ '{"product_changes":[{"guid":"product-1","action":"update","errands":[]}]}' } + let(:response){ pending_changes } + let(:status_code){ 200 } + let(:uri){ "#{base_uri}/api/v0/staged/pending_changes" } + + before do + stub_request(:get, uri).to_return(status: status_code, body: response_body) + end + + it "should run successfully" do + expect(response.code).to eq("200") + end + + it "should include product changes in its body" do + expected_value = JSON.parse('{"product_changes":[{"guid":"product-1","action":"update","errands":[]}]}') + expect(parsed_response).to eq(expected_value) + end + end + + describe '#wait_for_https_alive' do + before do + stub_request(:get, "#{base_uri}/").to_return(:status => 200, :body => "", :headers => {}) + end + + it 'returns an http response on success' do + response = opsman.wait_for_https_alive(1) + expect(response).to be_a Net::HTTPOK + expect(response.code.to_i).to equal(200) + end + + it 'times out after and returns nil' do + allow(opsman).to receive(:get).and_raise(Errno::ECONNREFUSED) + response = opsman.wait_for_https_alive(2) + expect(response).to be_a Net::HTTPInternalServerError + expect(opsman.instance_eval { @retry_counter }).to equal(2) + end + + it 'retries and returns success in the middle' do + raise_initial = true + allow(opsman).to receive(:get) do + if raise_initial + raise_initial = false + raise Errno::ECONNREFUSED.new() + else + Net::HTTPOK.new(1.0, 200, "OK") + end + end + response = opsman.wait_for_https_alive(2) + expect(response).to be_a Net::HTTPOK + expect(response.code.to_i).to equal(200) + expect(opsman.instance_eval { @retry_counter }).to equal(1) + end + end end diff --git a/spec/ops_manager/appliance/aws_spec.rb b/spec/ops_manager/appliance/aws_spec.rb new file mode 100644 index 0000000..093a11d --- /dev/null +++ b/spec/ops_manager/appliance/aws_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe OpsManager::Appliance::AWS do + let!(:aws){ described_class.new(config) } + let!(:connection) do + Fog::Compute.new({ + provider: 'AWS', + aws_access_key_id: 'key', + aws_secret_access_key: 'secret' + }) + end + let!(:config) do + { + name: 'ops-manager-aws', + provider: 'AWS', + desired_version: '1.4.11.0', + ip: '10.0.2.24', + admin_username: 'admin', + admin_password: 'admin', + opts: { + region: 'us-east-1', + availability_zone: 'us-east-1b', + ami_mapping_file: 'ami/*.yml', + instance_type: 'm4.medium', + ssh_keypair_name: keypair_name, + security_groups: ['sec-group-1','sec-group-2'], + subnet_id: subnet_id, + disk_size_in_gb: '100', + instance_profile_name: 'opsman-profile', + + access_key: 'key', + secret_key: 'secret', + } + } + end + + let!(:keypair_name) do + connection.key_pairs.create(name: "test-keypair").name + end + + let!(:vpc_id) do + connection.create_vpc('10.0.0.0/8').data[:body]['vpcSet'][0]['vpcId'] + end + let!(:subnet_id) do + connection.create_subnet(vpc_id, '10.0.2.0/24', { + AvailabilityZone: 'us-east-1b', + }).data[:body]['subnet']['subnetId'] + end + let!(:security_groups_ids) do + config[:opts][:security_groups].collect do |sg| + connection.create_security_group(sg,sg, vpc_id).data[:body]['groupId'] + end + end + + before(:all) do + Fog.mock! + end + after(:each) do + Fog::Mock.reset + end + + describe '#deploy_vm' do + it 'should create a vm with the proper config' do + server = nil + + expect do + server = aws.deploy_vm + end.to change{ connection.servers.count }.from(0).to(1) + + expect(server.tags["Name"]).to eq('ops-manager-aws-1.4.11.0') + expect(server.flavor_id).to eq(config[:opts][:instance_type]) + expect(server.subnet_id).to eq(config[:opts][:subnet_id]) + expect(server.key_name).to eq(config[:opts][:ssh_keypair_name]) + expect(server.private_ip_address).to eq(config[:ip]) + expect(server.associate_public_ip).to eq(false) + expect(server.availability_zone).to eq(config[:opts][:availability_zone]) + expect(server.image_id).to eq('ami-a26bacd8') + disk = connection.volumes.get(server.block_device_mapping[0]["volumeId"]) + expect(disk.size).to eq(config[:opts][:disk_size_in_gb]) + + # These tests fail due to bugs in the Fog Mock: + # https://github.com/fog/fog-aws/issues/404 + skip "Cannot test security groups, instance profiles, vpc id, or lack of public IP due to https://github.com/fog/fog-aws/issues/404" do + expect(server.security_group_ids).to eq(security_groups_ids) + expect(server.iam_instance_profile["arn"]).to end_with(":instance-profile/#{config[:opts][:instance_profile_name]}") + expect(server.vpc_id).to eq(vpc_id) + expect(server.public_ip_address).to be_nil + end + end + end + + describe '#deploy vm using instance profiles' do + let!(:connection) do + Fog::Compute.new({ + provider: "AWS", + use_iam_profile: true, + aws_access_key_id: "", + aws_secret_access_key: "", + }) + end + + it 'should use instance profile roles rather than a keypair, if provided' do + config[:opts][:use_iam_profile] = true + config[:opts][:access_key] = "" + config[:opts][:secret_key] = "" + expect do + aws.deploy_vm + end.to change{ connection.servers.count}. from(connection.servers.count).to(connection.servers.count + 1) + + expect(aws.instance_eval { @connection.instance_eval { @use_iam_profile }}).to eq(true) + end + end + + describe '#stop_current_vm' do + server = nil + before do + server = aws.deploy_vm + expect(server).not_to be_nil + end + it 'should stop a vm, image it, and then destroy it, to free up the IP' do + expect do + aws.stop_current_vm('ops-manager-aws-1.4.11.0') + end.to change{ + connection.images.all().count + }.from(0).to(1) + + expect(connection.servers.get(server.id)).to be_nil + end + end +end diff --git a/spec/ops_manager/appliance/vsphere_spec.rb b/spec/ops_manager/appliance/vsphere_spec.rb new file mode 100644 index 0000000..823f7d9 --- /dev/null +++ b/spec/ops_manager/appliance/vsphere_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe OpsManager::Appliance::Vsphere do + let(:vsphere){ described_class.new(config) } + let(:config) do + { + name: 'ops-manager', + provider: 'vsphere', + desired_version: '1.4.11.0', + ip: '1.2.3.4', + username: 'foo', + password: 'bar', + pivnet_token: 'abc123', + opts: { + ova_path: 'ops-manager.ova', # you can also specify the path with *. e.g.: ops-manager-ova/*.ova. This is usefull when using concourse and the pivnet-resource + portgroup: 'dummy-portgroup', + netmask: '255.255.255.0', + gateway: '1.2.3.1', + dns: '8.8.8.8', + datastore: 'DS1', + vcenter: { + username: 'VM_VCENTER_USER', + password: 'VM_VCENTER_PASSWORD', + host: '1.2.3.2', + datacenter: 'VM_DATACENTER', + cluster: 'VM_CLUSTER' + }, + ntp_servers: [ 'clock1.example.com', 'clock2.example.com'] + } + } + end + let(:current_version){ '1.4.2.0' } + let(:current_vm_name){ "ops-manager-1.4.2.0"} + + before do + OpsManager.set_conf(:target, '1.2.3.4') + OpsManager.set_conf(:username, 'foo') + OpsManager.set_conf(:username, 'bar') + end + + + xit 'should include logging' do + expect(vsphere).to be_kind_of(OpsManager::Logging) + end + + describe 'deploy_vm' do + subject(:deploy_vm){ vsphere.deploy_vm } + let(:vcenter_target){"vi://VM_VCENTER_USER:VM_VCENTER_PASSWORD@1.2.3.2/VM_DATACENTER/host/VM_CLUSTER"} + + it 'should run ovftools successfully' do + expect(vsphere).to receive(:`).with("echo yes | ovftool --acceptAllEulas --noSSLVerify --powerOn --X:waitForIp --net:\"Network 1=#{config[:opts][:portgroup]}\" --name=ops-manager-1.4.11.0 -ds=#{config[:opts][:datastore]} --prop:ip0=1.2.3.4 --prop:netmask0=#{config[:opts][:netmask]} --prop:gateway=#{config[:opts][:gateway]} --prop:DNS=#{config[:opts][:dns]} --prop:ntp_servers=#{config[:opts][:ntp_servers].join(',')} --prop:admin_password=#{config[:password]} #{config[:opts][:ova_path]} #{vcenter_target}") + deploy_vm + end + + %i{username password}.each do |m| + describe "when vcenter_#{m} has unescaped character" do + before { config[:opts][:vcenter][m] = "domain\\vcenter_+)#{m}" } + + + it "should URL encode the #{m}" do + expect(vsphere).to receive(:`).with(/domain\\%5Cvcenter_\\\+\\\)#{m}/) + deploy_vm + end + end + end + end + + describe 'stop_current_vm' do + let(:vm_name){ 'ops-manager-1.4.2.0' } + subject(:stop_current_vm) { vsphere.stop_current_vm(vm_name) } + + xit 'should stops current vm to release IP' do + VCR.use_cassette 'stopping vm' do + expect(RbVmomi::VIM).to receive(:connect).with({ host: vcenter_host, user: vcenter_username , password: vcenter_password , insecure: true}).and_call_original + expect_any_instance_of(RbVmomi::VIM::ServiceInstance).to receive(:find_datacenter).with(vcenter_datacenter).and_call_original + expect_any_instance_of(RbVmomi::VIM::Datacenter).to receive(:find_vm).with(current_vm_name).and_call_original + expect_any_instance_of(RbVmomi::VIM::VirtualMachine).to receive(:PowerOffVM_Task).and_call_original + stop_current_vm + end + end + end +end diff --git a/spec/ops_manager/appliance_deployment_spec.rb b/spec/ops_manager/appliance_deployment_spec.rb index d1a4e1b..0164973 100644 --- a/spec/ops_manager/appliance_deployment_spec.rb +++ b/spec/ops_manager/appliance_deployment_spec.rb @@ -9,6 +9,7 @@ let(:desired_version){ OpsManager::Semver.new('1.5.5') } let(:pivnet_api){ object_double(OpsManager::Api::Pivnet.new) } let(:opsman_api){ object_double(OpsManager::Api::Opsman.new) } + let(:appliance){ object_double(OpsManager::Appliance::Base.new(config_file))} let(:username){ 'foo' } let(:password){ 'foo' } let(:pivnet_token){ 'asd123' } @@ -34,6 +35,8 @@ allow(OpsManager::Api::Pivnet).to receive(:new).and_return(pivnet_api) allow(OpsManager::Api::Opsman).to receive(:new).and_return(opsman_api) + allow(OpsManager::Appliance::Vsphere).to receive(:new).and_return(appliance) + allow(OpsManager::Appliance::AWS).to receive(:new).and_return(appliance) allow(OpsManager::InstallationRunner).to receive(:trigger!).and_return(installation) allow(appliance_deployment).to receive(:current_version).and_return(current_version) @@ -48,20 +51,46 @@ end end - %w{ stop_current_vm deploy_vm }.each do |m| - describe m do - it 'should raise not implemented error' do - expect{ appliance_deployment.send(m) }.to raise_error(NotImplementedError) - end + describe '#deploy' do + subject(:deploy){ appliance_deployment.deploy } + + it 'Should perform in the right order' do + expect(appliance).to receive(:deploy_vm).ordered + expect(opsman_api).to receive(:wait_for_https_alive).ordered + + deploy end end - describe '#deploy' do + describe '#stop_current_vm' do + subject(:stop_current_vm){ appliance.stop_current_vm('ops-manager-1.5.5') } + it 'Should perform in the right order' do - %i( deploy_vm).each do |method| - expect(appliance_deployment).to receive(method).ordered + expect(appliance).to receive(:stop_current_vm).ordered + stop_current_vm + end + end + + describe '#appliance' do + before do + allow(OpsManager::Appliance::Vsphere).to receive(:new).and_call_original + allow(OpsManager::Appliance::AWS).to receive(:new).and_call_original + end + + describe 'when provider is appliance' do + before{ config[:provider] = 'vsphere' } + + it 'Should create an OpsManager::Appliance::Vsphere' do + expect(appliance_deployment.appliance).to be_kind_of(OpsManager::Appliance::Vsphere) + end + + end + describe 'when provider is aws' do + before{ config[:provider] = 'aws' } + + it 'Should create an OpsManager::Appliance::AWS' do + expect(appliance_deployment.appliance).to be_kind_of(OpsManager::Appliance::AWS) end - appliance_deployment.deploy end end @@ -70,23 +99,30 @@ before do %i( get_installation_assets get_installation_settings get_diagnostic_report ).each do |m| - allow(opsman_api).to receive(m) + allow(opsman_api).to receive(m) end - %i( download_current_stemcells - stop_current_vm deploy upload_installation_assets - provision_stemcells).each do |m| - allow(appliance_deployment).to receive(m) - end + %i( download_current_stemcells).each do |m| + allow(appliance_deployment).to receive(m) + end + + allow(appliance).to receive(:stop_current_vm) + + %i( deploy upload_installation_assets + wait_for_uaa provision_stemcells).each do |m| + allow(appliance_deployment).to receive(m) + end end it 'Should perform in the right order' do - %i( get_installation_assets download_current_stemcells - stop_current_vm deploy upload_installation_assets - provision_stemcells).each do |m| - expect(appliance_deployment).to receive(m).ordered - end - upgrade + %i( get_installation_assets download_current_stemcells).each do |m| + expect(appliance_deployment).to receive(m).ordered + end + expect(appliance).to receive(:stop_current_vm) + %i( deploy upload_installation_assets wait_for_uaa provision_stemcells).each do |m| + expect(appliance_deployment).to receive(m).ordered + end + upgrade end it 'should trigger installation' do @@ -105,11 +141,15 @@ let(:version){ "3062" } let(:other_version){ "3063" } + let(:windows_version){ "1200" } let(:installation_settings) do { "products" => [ - { "stemcell": { "version" => version } }, - { "stemcell": { "version" => other_version } }, + { "stemcell": { "version" => version, "os" => "ubuntu-trusty"} }, + { "stemcell": { "version" => other_version, "os" => "ubuntu-trusty"} }, + { "stemcell": { "version" => version, "os" => "ubuntu-trusty"} }, + { "stemcell": { "version" => version, "os" => "ubuntu-trusty"} }, + { "stemcell": { "version" => windows_version, "os" => "windowsR2012"} }, ] } end @@ -121,14 +161,18 @@ end describe 'when installation_settings are present' do - it 'should return list of current stemcells' do - expect(list_current_stemcells).to eq( [ version, other_version ]) + it 'should return uniqued list of current stemcells, with version and their corresponding product' do + expect(list_current_stemcells).to eq([ + {version: version, product: "stemcells"}, + {version: other_version, product: "stemcells"}, + {version: windows_version, product: "stemcells-windows-server" }, + ]) end end end describe '#find_stemcell_release' do - subject(:find_stemcell_release){ appliance_deployment.find_stemcell_release(stemcell_version) } + subject(:find_stemcell_release){ appliance_deployment.find_stemcell_release(stemcell_version, "arbitrary-stemcells") } let(:product_releases_response) do { 'releases' => [ @@ -142,7 +186,7 @@ before do allow(appliance_deployment).to receive(:get_product_releases) - .with('stemcells') + .with('arbitrary-stemcells') .and_return(double(status_code: 200, body: product_releases_response.to_json)) end @@ -164,7 +208,7 @@ end describe '#find_stemcell_file' do - subject(:find_stemcell_file){ appliance_deployment.find_stemcell_file(1, /vsphere/) } + subject(:find_stemcell_file){ appliance_deployment.find_stemcell_file(1, /vsphere/, "arbitrary-stemcell") } let(:stemcell_version){ "3062" } let(:product_file_id){ 1 } @@ -186,7 +230,7 @@ before do allow(appliance_deployment).to receive(:get_product_release_files) - .with('stemcells', 1) + .with('arbitrary-stemcell', 1) .and_return(double(status_code: 200, body: product_files_response.to_json)) end @@ -204,24 +248,32 @@ describe '#download_current_stemcells' do subject(:download_current_stemcells){ appliance_deployment.download_current_stemcells } - let(:current_stemcells){ ["3062.0" , "3063.0" ] } + let(:current_stemcells){ [ + {version: "3062.0", product: "stemcells"}, + {version: "3063.0", product: "stemcells"}, + {version: "1200.12", product: "stemcells-windows-server"}, + ]} let(:release_id){ rand(1000..9999) } let(:file_id) { rand(1000..9999) } let(:stemcell_filepath){ "bosh-stemcell-3062.0-vcloud-esxi-ubuntu-trusty-go_agent.tgz" } + let(:windows_filepath){ "light-bosh-stemcell-1200.12-vsphere-xen-hvm-windows2012R2-go_agent.tgz" } before do allow(appliance_deployment).tap do |ad| ad.to receive(:list_current_stemcells).and_return(current_stemcells) ad.to receive(:find_stemcell_release).and_return(release_id) - ad.to receive(:find_stemcell_file).with(release_id, /vsphere/).and_return([file_id, stemcell_filepath]) + ad.to receive(:find_stemcell_file).with(release_id, /vsphere/, "stemcells").and_return([file_id, stemcell_filepath]) + ad.to receive(:find_stemcell_file).with(release_id, /vsphere/, "stemcells-windows-server").and_return([file_id, windows_filepath]) ad.to receive(:accept_product_release_eula) ad.to receive(:download_product_release_file) end end - it 'should download all stemcell' do + it 'should download all stemcells from the appropriate products' do expect(appliance_deployment).to receive(:download_product_release_file) .with('stemcells', release_id, file_id, write_to: "/tmp/current_stemcells/#{stemcell_filepath}" ).twice + expect(appliance_deployment).to receive(:download_product_release_file) + .with('stemcells-windows-server', release_id, file_id, write_to: "/tmp/current_stemcells/#{windows_filepath}" ) download_current_stemcells end @@ -239,7 +291,8 @@ .and_return([ '/tmp/current_stemcells/stemcell-1.tgz', '/tmp/current_stemcells/stemcell-2.tgz', - ]) + ]) + allow(opsman_api).to receive(:reset_access_token) end it 'should upload all the stemcells in /tmp/current_stemcells' do @@ -249,6 +302,50 @@ .with('/tmp/current_stemcells/stemcell-2.tgz') provision_stemcells end + + it 'should reset the opsman token before running imports' do + expect(opsman_api).to receive(:reset_access_token).ordered + expect(opsman_api).to receive(:import_stemcell).ordered.twice + provision_stemcells + end + end + + describe '#wait_for_uaa' do + subject(:wait_for_uaa){ appliance_deployment.wait_for_uaa } + + before do + allow(appliance_deployment).to receive(:sleep) + end + + + describe 'when uaa is available' do + before do + allow(opsman_api).to receive(:get_ensure_availability) + .and_return(double( code:'302', body:'You are being /auth/cloudfoundry redirected')) + end + + it 'should exit successfully' do + expect(opsman_api).to receive(:get_ensure_availability) + wait_for_uaa + end + end + + describe 'when uaa is not available yet' do + before do + allow(opsman_api).to receive(:get_ensure_availability) + .and_return( + double( code:'503', body:'503 Bad Gateway'), + double( code:'302', body:'Ops Manager Setup'), + double( code:'200', body:'Waiting for authentication system to start...'), + double( code:'302', body:'You are being /auth/cloudfoundry redirected') + ) + end + + it 'should wait until uaa is ready' do + expect(opsman_api).to receive(:get_ensure_availability).exactly(4).times + wait_for_uaa + end + end end describe '#provision_stemcells' do @@ -316,7 +413,12 @@ describe 'when ops-manager has been deployed and current and desired version match' do let(:desired_version){ current_version } + let(:pending_changes_response){ { "product_changes": [] }} + before do + allow(appliance_deployment).to receive(:get_pending_changes) + .and_return(double(status_code: 200, body: pending_changes_response.to_json)) + end it 'does not performs a deployment' do expect(appliance_deployment).to_not receive(:deploy) expect do @@ -330,6 +432,17 @@ run end.to output(/OpsManager at #{target} version is already #{current_version.to_s}. Skiping .../).to_stdout end + + describe 'when there are pending changes' do + let(:pending_changes_response){ {"product_changes": [{ "guid": "cf" }]} } + + it 'should apply changes' do + expect(OpsManager::InstallationRunner).to receive(:trigger!) + expect do + run + end.to output(/OpsManager at #{target} version has pending changes. Applying changes.../).to_stdout + end + end end describe 'when current version is older than desired version' do diff --git a/spec/ops_manager/cli_spec.rb b/spec/ops_manager/cli_spec.rb index 5dc6fc8..12b5017 100644 --- a/spec/ops_manager/cli_spec.rb +++ b/spec/ops_manager/cli_spec.rb @@ -88,6 +88,17 @@ end end + describe 'pending-changes' do + let(:args) { %w(pending-changes) } + let(:pending_changes_response) { 'No pending changes' } + + it "should call ops_manager.pending_changes" do + allow(opsman_api).to receive(:pending_changes).and_return(pending_changes_response) + expect_any_instance_of(OpsManager::Cli::PendingChanges).to receive(:run) + cli.run(`pwd`, args) + end + end + describe 'import-stemcell' do let(:args) { %w(import-stemcell /tmp/is.yml) } diff --git a/spec/ops_manager/config/base_spec.rb b/spec/ops_manager/config/base_spec.rb new file mode 100644 index 0000000..bdb6311 --- /dev/null +++ b/spec/ops_manager/config/base_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' +require 'ops_manager/config/base' + +describe OpsManager::Config::Base do + let!(:base){ described_class.new({filepath: filepath}) } + + describe '#expand_path_for!' do + describe 'when filepath is a regex' do + let(:filepath){ '*.pivotal' } + + it 'should return first mathing path' do + expect do + base.expand_path_for!(:filepath) + end.to change{base[:filepath]}.from(filepath).to('tile.pivotal') + end + end + + describe 'when key is not present' do + let!(:base){ described_class.new({}) } + + it 'should ignore the key' do + expect do + base.expand_path_for!(:unknown_key) + end.not_to raise_error(TypeError) + end + end + end + + + describe '#validate_presence_of!' do + let(:filepath){ 'tile.pivotal' } + + it 'should success when attr is present' do + expect do + base.validate_presence_of!(:filepath) + end.not_to raise_error + end + + it 'should error when attr is missing' do + expect do + base.validate_presence_of!(:missing_attr) + end.to raise_error + end + end +end diff --git a/spec/ops_manager/configs/opsman_deployment_spec.rb b/spec/ops_manager/config/opsman_deployment_spec.rb similarity index 60% rename from spec/ops_manager/configs/opsman_deployment_spec.rb rename to spec/ops_manager/config/opsman_deployment_spec.rb index 6b7e018..7ba8779 100644 --- a/spec/ops_manager/configs/opsman_deployment_spec.rb +++ b/spec/ops_manager/config/opsman_deployment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -require 'ops_manager/configs/opsman_deployment' +require 'ops_manager/config/opsman_deployment' -describe OpsManager::Configs::OpsmanDeployment do +describe OpsManager::Config::OpsmanDeployment do let(:opsman_deployment_config){ described_class.new(config) } let(:config) do { @@ -12,11 +12,21 @@ 'password' => 'bar', 'ip' => '1.2.3.4', 'pivnet_token' => 'abc123', - 'opts' => {} + 'opts' => { + 'vcenter' => { + 'host' => '1.2.3.4', + 'username' => 'foo', + 'password' => 'bar', + } + } } end - it "should not error when configs are correct" do + it 'should symbolize recursively the configs' do + expect(opsman_deployment_config[:opts][:vcenter][:host]).to eq('1.2.3.4') + end + + it 'should not error when configs are correct' do expect do opsman_deployment_config end.not_to raise_error diff --git a/spec/ops_manager/configs/product_deployment_spec.rb b/spec/ops_manager/config/product_deployment_spec.rb similarity index 64% rename from spec/ops_manager/configs/product_deployment_spec.rb rename to spec/ops_manager/config/product_deployment_spec.rb index 0aa260b..fe3843f 100644 --- a/spec/ops_manager/configs/product_deployment_spec.rb +++ b/spec/ops_manager/config/product_deployment_spec.rb @@ -1,14 +1,16 @@ require 'spec_helper' -require 'ops_manager/configs/product_deployment' +require 'ops_manager/config/product_deployment' -describe OpsManager::Configs::ProductDeployment do +describe OpsManager::Config::ProductDeployment do let(:product_deployment_config){ described_class.new(config) } let(:stemcell){ 'stemcell.tgz' } + let(:filepath){ 'tile.pivotal' } let(:config) do { 'name' => 'example-product', 'desired_version' => '1.6.2.0', - 'stemcell' => stemcell + 'stemcell' => stemcell, + 'filepath' => filepath } end @@ -36,4 +38,14 @@ end end end + + describe '#filepath' do + describe 'when filepath is a regex' do + let(:filepath){ '*.pivotal' } + + it 'should return first mathing path' do + expect(product_deployment_config.filepath).to eq('tile.pivotal') + end + end + end end diff --git a/spec/ops_manager/configs/base_spec.rb b/spec/ops_manager/configs/base_spec.rb deleted file mode 100644 index 9d16676..0000000 --- a/spec/ops_manager/configs/base_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'spec_helper' -require 'ops_manager/configs/base' - -describe OpsManager::Configs::Base do - let(:base){ described_class.new(config) } - let(:config) do - described_class.new( - 'filepath' => filepath - ) - end - - describe '#filepath' do - describe 'when filepath is a regex' do - let(:filepath){ '*.pivotal' } - - it 'should return first mathing path' do - expect(config.filepath).to eq('example-product-1.6.1.pivotal') - end - end - end - - describe '#find_full_path' do - describe 'when filepath is nil' do - let(:filepath){ nil } - before{ allow(base).to receive(:`).and_return('.') } - - it 'should return nil' do - expect(base.find_full_path(filepath)).to be_nil - end - end - end -end diff --git a/spec/ops_manager/deployments/vsphere_spec.rb b/spec/ops_manager/deployments/vsphere_spec.rb deleted file mode 100644 index 38e7532..0000000 --- a/spec/ops_manager/deployments/vsphere_spec.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'spec_helper' - -describe OpsManager::Deployments::Vsphere do - class Foo ; include OpsManager::Deployments::Vsphere ; end - let(:conf_file){'ops_manager_deployment.yml'} - let(:conf){ YAML.load_file(conf_file) } - let(:name){ conf.fetch('name') } - let(:target){ conf.fetch('ip') } - let(:username){ conf.fetch('username') } - let(:password){ conf.fetch('password') } - let(:vcenter_username){ vcenter.fetch('username') } - let(:vcenter_password){ vcenter.fetch('password') } - let(:vcenter_datacenter){ vcenter.fetch('datacenter') } - let(:vcenter_cluster){ vcenter.fetch('cluster') } - let(:vcenter_host){ vcenter.fetch('host') } - let(:vcenter){ config.opts.fetch('vcenter') } - let(:config){ OpsManager::Configs::OpsmanDeployment.new(::YAML.load_file('ops_manager_deployment.yml') )} - let(:current_version){ '1.4.2.0' } - let(:current_vm_name){ "#{name}-#{current_version}"} - let(:vm_name){ 'ops-manager-1.4.11.0' } - let(:vsphere){ Foo.new } - - before do - allow(vsphere).to receive(:config).and_return(config) - OpsManager.set_conf(:target, '1.2.3.4') - OpsManager.set_conf(:username, 'foo') - OpsManager.set_conf(:username, 'bar') - end - - - xit 'should include logging' do - expect(vsphere).to be_kind_of(OpsManager::Logging) - end - - describe 'deploy_vm' do - subject(:deploy_vm){ vsphere.deploy_vm(vm_name, target) } - let(:vcenter_target){"vi://#{vcenter_username}:#{vcenter_password}@#{vcenter_host}/#{vcenter_datacenter}/host/#{vcenter_cluster}"} - - it 'should run ovftools successfully' do - expect(vsphere).to receive(:`).with("echo yes | ovftool --acceptAllEulas --noSSLVerify --powerOn --X:waitForIp --net:\"Network 1=#{config.opts['portgroup']}\" --name=#{vm_name} -ds=#{config.opts['datastore']} --prop:ip0=#{target} --prop:netmask0=#{config.opts['netmask']} --prop:gateway=#{config.opts['gateway']} --prop:DNS=#{config.opts['dns']} --prop:ntp_servers=#{config.opts['ntp_servers'].join(',')} --prop:admin_password=#{password} #{config.opts['ova_path']} #{vcenter_target}") - deploy_vm - end - - %w{username password}.each do |m| - describe "when vcenter_#{m} has unescaped character" do - before { config.opts['vcenter'][m] = "domain\\vcenter_+)#{m}" } - - it "should URL encode the #{m}" do - expect(vsphere).to receive(:`).with(/domain\\%5Cvcenter_\\\+\\\)#{m}/) - deploy_vm - end - end - end - end - - describe 'stop_current_vm' do - let(:vm_name){ 'ops-manager-1.4.2.0' } - subject(:stop_current_vm) { vsphere.stop_current_vm(vm_name) } - - xit 'should stops current vm to release IP' do - VCR.use_cassette 'stopping vm' do - expect(RbVmomi::VIM).to receive(:connect).with({ host: vcenter_host, user: vcenter_username , password: vcenter_password , insecure: true}).and_call_original - expect_any_instance_of(RbVmomi::VIM::ServiceInstance).to receive(:find_datacenter).with(vcenter_datacenter).and_call_original - expect_any_instance_of(RbVmomi::VIM::Datacenter).to receive(:find_vm).with(current_vm_name).and_call_original - expect_any_instance_of(RbVmomi::VIM::VirtualMachine).to receive(:PowerOffVM_Task).and_call_original - stop_current_vm - end - end - end -end diff --git a/spec/ops_manager/director_template_generator_spec.rb b/spec/ops_manager/director_template_generator_spec.rb index 14a83e2..958a261 100644 --- a/spec/ops_manager/director_template_generator_spec.rb +++ b/spec/ops_manager/director_template_generator_spec.rb @@ -98,13 +98,6 @@ it 'should remove guid' do expect(generated_template).not_to have_key('guid') end - it 'should not include product director_ssl' do - expect(generated_template['products'][1]).not_to have_key('director_ssl') - end - - it 'should not include product uaa_ssl' do - expect(generated_template['products'][1]).not_to have_key('uaa_ssl') - end it 'should remove uaa_credentials' do expect(generated_template['products'][1]).not_to have_key('uaa_credentials') diff --git a/spec/ops_manager/installation_runner_spec.rb b/spec/ops_manager/installation_runner_spec.rb index 7c4b999..399f808 100644 --- a/spec/ops_manager/installation_runner_spec.rb +++ b/spec/ops_manager/installation_runner_spec.rb @@ -4,32 +4,33 @@ describe OpsManager::InstallationRunner do let(:installation_runner){ described_class.new } let(:installation_id){ rand(9999) } + let(:errand_name){ "errand#{rand(9999)}" } let(:installation_response){ double('installation_response', body: "{\"install\":{\"id\":#{installation_id}}}" ) } - let(:product_guid){ "product1" } + let(:product_guid){ "product_1_guid" } let(:opsman_api){ double.as_null_object } let(:get_staged_products_response){ double(body: '[]') } - let(:get_staged_products_errands_response){ double(body: '{ "errands": []}') } + let(:staged_products_errands_response){ double(body: '{ "errands": []}') } before do allow(OpsManager::Api::Opsman).to receive(:new).and_return(opsman_api) allow(opsman_api).to receive(:trigger_installation).and_return(installation_response) allow(opsman_api).to receive(:get_staged_products).and_return(get_staged_products_response) - allow(opsman_api).to receive(:get_staged_products_errands).with(product_guid).and_return(get_staged_products_errands_response) + allow(opsman_api).to receive(:get_staged_products_errands).with(product_guid).and_return(staged_products_errands_response) end describe '#trigger' do subject(:trigger){ installation_runner.trigger! } let(:get_staged_products_response){ double( body: [ { "guid" => product_guid }].to_json, code: 200) } - let(:get_staged_products_errands_response) do + let(:staged_products_errands_response) do double( code: '200', body: { "errands" => [ - { "name" => "errand1", + { "name" => errand_name, "post_deploy" => true, "pre_delete" => false }, - { "name" => "errand2", + { "name" => "pre_deploy_errand", "post_deploy" => false, "pre_delete" => true }]}.to_json ) @@ -48,16 +49,32 @@ it 'should set enable_errands for all products' do expect(opsman_api).to receive(:trigger_installation) - .with(body: 'ignore_warnings=true&enabled_errands[product1][post_deploy_errands][]=errand1') + .with( + headers: {"Content-Type" => "application/json"}, + body: { + "errands"=> { + "product_1_guid"=> { + "run_post_deploy"=> { + errand_name => true + } + } + }, + "ignore_warnings"=> true + }.to_json ) trigger end - describe 'when product errands endpoint does not exists' do - let(:get_staged_products_errands_response){ double(body: '', code: 404) } + describe 'when product errands do not exist' do + let(:staged_products_errands_response){ double(body: '', code: 404) } - it 'should set enable_errands for all products' do + it 'should not return any errands' do expect(opsman_api).to receive(:trigger_installation) - .with(body: 'ignore_warnings=true&enabled_errands[product1]{}') + .with( + headers: {"Content-Type" => "application/json"}, + body: { + "errands"=> {}, + "ignore_warnings"=> true + }.to_json ) trigger end diff --git a/spec/ops_manager/logging_spec.rb b/spec/ops_manager/logging_spec.rb index 4ec2a51..4c6dbeb 100644 --- a/spec/ops_manager/logging_spec.rb +++ b/spec/ops_manager/logging_spec.rb @@ -19,13 +19,8 @@ class Foo; include OpsManager::Logging; end expect(foo.logger).to eq(foo.logger) end - it 'should output logs to stdout' do - expect(Logger).to receive(:new).with(STDOUT).and_call_original - foo.logger - end - - it 'should log in WARN level' do - expect_any_instance_of(Logger).to receive(:level=).with(Logger::WARN) + it 'should output logs to stdout with default log level WARN' do + expect(Logger).to receive(:new).with(STDERR, level: Logger::WARN).and_call_original foo.logger end @@ -33,7 +28,7 @@ class Foo; include OpsManager::Logging; end before { ENV['DEBUG']= 'true' } it 'should log in INFO level' do - expect_any_instance_of(Logger).to receive(:level=).with(Logger::INFO) + expect(Logger).to receive(:new).with(STDERR, level: Logger::INFO).and_call_original foo.logger end diff --git a/spec/ops_manager/product_deployment_spec.rb b/spec/ops_manager/product_deployment_spec.rb index 19ba3cb..126a0f0 100644 --- a/spec/ops_manager/product_deployment_spec.rb +++ b/spec/ops_manager/product_deployment_spec.rb @@ -8,7 +8,7 @@ let(:target){'1.2.3.4'} let(:username){ 'foo' } let(:password){ 'bar' } - let(:filepath) { 'example-product-1.6.1.pivotal' } + let(:filepath) { 'tile.pivotal' } let(:guid) { 'example-product-abc123' } let(:installation_settings_file){ '../fixtures/installation_settings.json' } let(:desired_version){ '1.6.2.0' } @@ -18,18 +18,19 @@ let(:product_installation){ OpsManager::ProductInstallation.new(guid, current_version, true) } let(:installation){ double.as_null_object } let(:config) do - OpsManager::Configs::ProductDeployment.new( - 'target' => target, - 'username' => username, - 'password' => password, - 'name' => name, - 'desired_version' => desired_version, - 'filepath' => filepath, - 'stemcell' => 'stemcell.tgz', - 'installation_settings_file' => installation_settings_file - ) + { + target: target, + username: username, + password: password, + name: name, + desired_version: desired_version, + filepath: filepath, + stemcell: 'stemcell.tgz', + installation_settings_file: installation_settings_file + } end + before do allow(product_deployment).tap do |pd| pd.to receive(:config).and_return(config) @@ -62,7 +63,7 @@ end it 'should spruce merge current installation settings with product installation settings' do - expect(product_deployment).to receive(:`).with("DEBUG=false spruce merge /tmp/is.yml #{installation_settings_file} > /tmp/new_is.yml") + expect(product_deployment).to receive(:`).with("DEBUG=false DEFAULT_ARRAY_MERGE_KEY=identifier spruce merge /tmp/is.yml #{installation_settings_file} > /tmp/new_is.yml") merge_product_installation_settings end @@ -157,7 +158,8 @@ let(:product_installation) do OpsManager::ProductInstallation.new(guid, '1.6.0.0', true) end - let(:filepath) { 'example-product-1.6.2.pivotal' } + + let(:filepath) { 'tile.pivotal' } let(:product_exists?){ true } before do allow(product_installation).to receive(:prepared?) diff --git a/spec/ops_manager/product_template_generator_spec.rb b/spec/ops_manager/product_template_generator_spec.rb index 0b09c50..bb9d387 100644 --- a/spec/ops_manager/product_template_generator_spec.rb +++ b/spec/ops_manager/product_template_generator_spec.rb @@ -20,9 +20,16 @@ def genpass(length); rand(36**length).to_s(36); end stemcell: { 'some' => 'stemcell meta deta' }, 'properties' => [ { + 'deployed' => false, 'value' => { 'private_key_pem' => 'Product Private Key' - } + }, + 'options' => [ + { + 'identifier' => 'internal_mysql', + 'properties' => [ { 'deployed' => false, 'identifier' => 'host' } ] + } + ] } ], 'jobs' => [ @@ -31,10 +38,17 @@ def genpass(length); rand(36**length).to_s(36); end 'partitions' => 'some partition info' , 'properties' => [ { + 'deployed' => false, 'value' => { 'identity' => 'conf-1', 'password' => random_password - } + }, + 'records' => [ + { + 'identifier' => 'internal_mysql', + 'properties' => [ { 'deployed' => false, 'identifier' => 'host' } ] + } + ] }, { 'value' => { @@ -68,8 +82,8 @@ def genpass(length); rand(36**length).to_s(36); end end describe '#generate_yml' do - let(:generated_hash){ { "products" => [ "(( merge on identifier ))", { 'identifier' => product_name } ] } } - let(:product_template){"---\nproducts:\n- (( merge on identifier ))\n- identifier: #{product_name}\n"} + let(:generated_hash){ { "products" => [ { 'identifier' => product_name } ] } } + let(:product_template){"---\nproducts:\n- identifier: #{product_name}\n"} before do allow(product_template_generator).to receive(:generate).and_return(generated_hash) @@ -123,12 +137,16 @@ def genpass(length); rand(36**length).to_s(36); end expect(generated_template.to_s).not_to match(random_secret) end - it 'should remove job properties private keys' do - expect(generated_template.to_s).not_to match('Job Private Key') + it 'should deployed flag from job properties and product properties' do + expect(generated_template.to_s).not_to match('deployed') + end + + it 'should should remove job properties private keys' do + expect(generated_template.to_s).to match('Job Private Key') end - it 'should remove product private keys' do - expect(generated_template.to_s).not_to match('Product Private Key') + it 'should should remove product private keys' do + expect(generated_template.to_s).to match('Product Private Key') end it 'should remove the product version' do @@ -136,7 +154,7 @@ def genpass(length); rand(36**length).to_s(36); end end it 'should remove stemcell metadata' do - expect(generated_template['products'][1]).not_to have_key('stemcell') + expect(generated_template['products'].first).not_to have_key('stemcell') end end end diff --git a/specs.4.8 b/specs.4.8 new file mode 100644 index 0000000..68ac13a Binary files /dev/null and b/specs.4.8 differ