Hello community, here is the log from the commit of package velum for openSUSE:Factory checked in at 2018-05-11 09:18:05 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/velum (Old) and /work/SRC/openSUSE:Factory/.velum.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "velum" Fri May 11 09:18:05 2018 rev:26 rq:606231 version:4.0.0+dev+git_r754_6c7835c7a3cc0999ebebf57517c32bf35bbd8bd0 Changes: -------- --- /work/SRC/openSUSE:Factory/velum/velum.changes 2018-05-10 15:50:51.222066861 +0200 +++ /work/SRC/openSUSE:Factory/.velum.new/velum.changes 2018-05-11 09:18:08.490805711 +0200 @@ -1,0 +2,45 @@ +Wed May 9 18:02:44 UTC 2018 - [email protected] + +- Commit 9d97b1d by James Mason [email protected] + Prevent double-clicking setup#bootstrap submit in public cloud + + Since that actually triggered construction of nodes, we need just a little + something to keep the impatient from clicking over & over. + + +------------------------------------------------------------------- +Wed May 9 17:44:25 UTC 2018 - [email protected] + +- Commit f24a3f1 by James Mason [email protected] + Include counts on cloud bootstrap jobs in discovery UI + + Commit f50a986 by James Mason [email protected] + Return cloud bootstrapping job progress in discovery + + BONUS: render discovery.html faster by not evaluating unused hash conditions. + + Commit 1c00b7b by James Mason [email protected] + Move cloud cluster building into model; capture job ids... + + ... clear failures on (re)start + + Commit 8fcedc8 by James Mason [email protected] + Add an event handler for completing cloud bootstrap + + Commit d50aad9 by James Mason [email protected] + Define a model for persisting salt job progress. + + +------------------------------------------------------------------- +Wed May 9 12:19:39 UTC 2018 - [email protected] + +- Commit 1f3adaa by Rafael Fernández López [email protected] + Make the `bin/init` process only do initialization operations + + Do not make this script run the puma server. This way, this command can be + run as an init container, leaving the server execution to the pod definition. + + Fixes: bsc#1091843 + + +------------------------------------------------------------------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ velum.spec ++++++ --- /var/tmp/diff_new_pack.u4HEZn/_old 2018-05-11 09:18:10.450734700 +0200 +++ /var/tmp/diff_new_pack.u4HEZn/_new 2018-05-11 09:18:10.454734555 +0200 @@ -23,7 +23,7 @@ # Version: 1.0.0 # %%define branch 1.0.0 -Version: 4.0.0+dev+git_r744_91ff022f1f12c133c9ccf131aa97bdd9091e4e50 +Version: 4.0.0+dev+git_r754_6c7835c7a3cc0999ebebf57517c32bf35bbd8bd0 Release: 0 %define branch master Summary: Dashboard for CaasP @@ -96,7 +96,7 @@ %description velum is the dashboard for CaasP to manage and deploy kubernetes clusters on top of MicroOS -This package has been built with commit 91ff022f1f12c133c9ccf131aa97bdd9091e4e50 from branch master on date Wed, 09 May 2018 07:04:56 +0000 +This package has been built with commit 6c7835c7a3cc0999ebebf57517c32bf35bbd8bd0 from branch master on date Wed, 09 May 2018 18:02:02 +0000 %prep %setup -q -n velum-%{branch} ++++++ 0_set_default_salt_events_alter_time_column_value.rpm.patch ++++++ --- /var/tmp/diff_new_pack.u4HEZn/_old 2018-05-11 09:18:10.486733396 +0200 +++ /var/tmp/diff_new_pack.u4HEZn/_new 2018-05-11 09:18:10.490733251 +0200 @@ -2,7 +2,7 @@ index b8392cd..6061543 100644 --- a/db/schema.rb +++ b/db/schema.rb -@@ -95,7 +95,7 @@ ActiveRecord::Schema.define(version: 20180406080400) do +@@ -95,7 +95,7 @@ ActiveRecord::Schema.define(version: 20180427014552) do create_table "salt_events", force: :cascade do |t| t.string "tag", limit: 255, null: false t.text "data", limit: 16777215, null: false @@ -11,12 +11,12 @@ t.string "master_id", limit: 255, null: false t.datetime "taken_at" t.datetime "processed_at" -@@ -113,7 +113,7 @@ ActiveRecord::Schema.define(version: 20180406080400) do +@@ -113,7 +113,7 @@ ActiveRecord::Schema.define(version: 20180427014552) do t.string "id", limit: 255, null: false t.string "success", limit: 10, null: false t.text "full_ret", limit: 16777215, null: false - t.datetime "alter_time", null: false + t.column "alter_time", "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" end - + add_index "salt_returns", ["fun"], name: "fun", using: :btree ++++++ master.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/assets/javascripts/cloud/bootstrap.js new/velum-master/app/assets/javascripts/cloud/bootstrap.js --- old/velum-master/app/assets/javascripts/cloud/bootstrap.js 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/assets/javascripts/cloud/bootstrap.js 2018-05-09 20:02:40.000000000 +0200 @@ -62,4 +62,9 @@ // kick things off $('input[name="cloud_cluster[instance_type]"][checked="checked"]').click(); + + // only submit once + $('form#new_cloud_cluster').submit(function(){ + $(this).find('input[type=submit]').prop('disabled', true); + }); }); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/assets/javascripts/dashboard/dashboard.js new/velum-master/app/assets/javascripts/dashboard/dashboard.js --- old/velum-master/app/assets/javascripts/dashboard/dashboard.js 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/assets/javascripts/dashboard/dashboard.js 2018-05-09 20:02:40.000000000 +0200 @@ -66,6 +66,8 @@ var unassignedMinions = data.unassigned_minions || []; var allMinions = minions.concat(unassignedMinions); var pendingMinions = data.pending_minions || []; + var pendingCloudJobs = data.pending_cloud_jobs || 0; + var cloudJobsFailed = data.cloud_jobs_failed || 0; // for the dashboard, if we rely on radio, the first time this comes // it won't detect that there's a master, so we need to rely on the data @@ -106,6 +108,34 @@ MinionPoller.initialized = true; } + // handle public cloud bootstrapping alerts + if (pendingCloudJobs > 0) { + $('#discovery-pending-cloud-jobs-count').text(pendingCloudJobs); + if (pendingCloudJobs == 1) { + $('#discovery-pending-cloud-jobs span.singular').removeClass('hidden'); + $('#discovery-pending-cloud-jobs span.plural').addClass('hidden'); + } else { + $('#discovery-pending-cloud-jobs span.plural').removeClass('hidden'); + $('#discovery-pending-cloud-jobs span.singular').addClass('hidden'); + } + $('#discovery-pending-cloud-jobs').removeClass('hidden'); + } else { + $('#discovery-pending-cloud-jobs').addClass('hidden'); + } + if (cloudJobsFailed > 0) { + $('#discovery-cloud-job-errors-count').text(cloudJobsFailed); + if (cloudJobsFailed == 1) { + $('#discovery-bootstrap-alert span.singular').removeClass('hidden'); + $('#discovery-bootstrap-alert span.plural').addClass('hidden'); + } else { + $('#discovery-bootstrap-alert span.plural').removeClass('hidden'); + $('#discovery-bootstrap-alert span.singular').addClass('hidden'); + } + $('#discovery-bootstrap-alert').removeClass('hidden'); + } else { + $('#discovery-bootstrap-alert').addClass('hidden'); + } + switch (MinionPoller.renderMode) { case "Discovery": minions = allMinions; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/controllers/concerns/discovery.rb new/velum-master/app/controllers/concerns/discovery.rb --- old/velum-master/app/controllers/concerns/discovery.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/controllers/concerns/discovery.rb 2018-05-09 20:02:40.000000000 +0200 @@ -7,22 +7,18 @@ # Responds with either an HTML or JSON version of the available minions. def discovery - assigned_minions = Minion.cluster_role - unassigned_minions = Minion.unassigned_role - pending_minions = ::Velum::Salt.pending_minions - retryable_bootstrap_orchestration = Orchestration.retryable? kind: :bootstrap - retryable_upgrade_orchestration = Orchestration.retryable? kind: :upgrade - respond_to do |format| format.html format.json do hsh = { - assigned_minions: assigned_minions, - unassigned_minions: unassigned_minions, - pending_minions: pending_minions, + assigned_minions: Minion.cluster_role, + unassigned_minions: Minion.unassigned_role, + pending_minions: ::Velum::Salt.pending_minions, + pending_cloud_jobs: SaltJob.all_open.count, + cloud_jobs_failed: SaltJob.failed.count, admin: Minion.find_by(minion_id: "admin"), - retryable_bootstrap_orchestration: retryable_bootstrap_orchestration, - retryable_upgrade_orchestration: retryable_upgrade_orchestration + retryable_bootstrap_orchestration: Orchestration.retryable?(kind: :bootstrap), + retryable_upgrade_orchestration: Orchestration.retryable?(kind: :upgrade) } render json: hsh end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/controllers/setup_controller.rb new/velum-master/app/controllers/setup_controller.rb --- old/velum-master/app/controllers/setup_controller.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/controllers/setup_controller.rb 2018-05-09 20:02:40.000000000 +0200 @@ -96,7 +96,7 @@ @cloud_cluster = CloudCluster.new(cloud_cluster_params) if @cloud_cluster.save - Velum::Salt.build_cloud_cluster(@cloud_cluster.instance_count) + @cloud_cluster.build! redirect_to setup_discovery_path, notice: "Starting to build #{@cloud_cluster}..." else diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/models/cloud_cluster.rb new/velum-master/app/models/cloud_cluster.rb --- old/velum-master/app/models/cloud_cluster.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/models/cloud_cluster.rb 2018-05-09 20:02:40.000000000 +0200 @@ -52,6 +52,18 @@ Velum::Salt.call(action: "saltutil.refresh_pillar") end + def build! + SaltJob.failed.destroy_all + return unless (responses = Velum::Salt.build_cloud_cluster(@instance_count)) + responses.each do |response| + if response.code.to_i == 500 + errors.add(:base, response.body) + else + SaltJob.create(jid: JSON.parse(response.body)["return"].first["jid"]) + end + end + end + def save save! return true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/models/salt_event.rb new/velum-master/app/models/salt_event.rb --- old/velum-master/app/models/salt_event.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/models/salt_event.rb 2018-05-09 20:02:40.000000000 +0200 @@ -12,7 +12,8 @@ SaltHandler::MinionStart, SaltHandler::MinionHighstate, SaltHandler::OrchestrationTrigger, - SaltHandler::OrchestrationResult + SaltHandler::OrchestrationResult, + SaltHandler::CloudBootstrap ].freeze scope :not_processed, -> { where(processed_at: nil) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/models/salt_handler/cloud_bootstrap.rb new/velum-master/app/models/salt_handler/cloud_bootstrap.rb --- old/velum-master/app/models/salt_handler/cloud_bootstrap.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/app/models/salt_handler/cloud_bootstrap.rb 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require "velum/salt" + +# This class is responsible to handle the salt events with tag "minion_start". +# When such an event occurs, we want the minion to be saved in our database +# if not already there. +class SaltHandler::CloudBootstrap + attr_reader :salt_event, :job + + TAG_MATCHER = %r{^salt\/job\/(?<jid>\d+)\/ret\/admin$} + + def self.can_handle_event?(event) + matches = TAG_MATCHER.match(event.tag) + return false unless matches + return false if event.parsed_data["fun"] != "cloud.profile" + SaltJob.all_open.jids.include? matches[:jid] + end + + def initialize(salt_event) + @salt_event = salt_event + jid = TAG_MATCHER.match(@salt_event.tag)[:jid] + @job = SaltJob.find_by(jid: jid) + end + + def process_event + parsed_data = salt_event.parsed_data + job.complete!(parsed_data["retcode"] || -1, master_trace: parsed_data["return"]) + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/models/salt_job.rb new/velum-master/app/models/salt_job.rb --- old/velum-master/app/models/salt_job.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/app/models/salt_job.rb 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,34 @@ +# Store selected salt job ids, and record on their result +class SaltJob < ActiveRecord::Base + validates :jid, uniqueness: true + + scope :all_open, -> { where(retcode: nil) } + scope :failed, -> { where.not(retcode: [nil, 0]) } + scope :jids, -> { pluck(:jid) } + + def complete!(retcode = 0, master_trace: nil, minion_trace: nil) + changes = { retcode: retcode } + changes[:master_trace] = master_trace if master_trace + changes[:minion_trace] = minion_trace if minion_trace + update_attributes! changes + + parse_upstream_error if failed? + end + + def completed? + retcode.present? + end + + def succeeded? + completed? && retcode.zero? + end + + def failed? + completed? && !retcode.zero? + end + + def parse_upstream_error + return if master_trace.blank? && minion_trace.blank? + errors.add(:base, "Please check `/var/log/salt/minion` for details.") + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/app/views/setup/discovery.html.slim new/velum-master/app/views/setup/discovery.html.slim --- old/velum-master/app/views/setup/discovery.html.slim 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/app/views/setup/discovery.html.slim 2018-05-09 20:02:40.000000000 +0200 @@ -1,3 +1,25 @@ +.alert.alert-info#discovery-pending-cloud-jobs.hidden role="alert" + i.fa.fa-4x.pull-left aria-hidden="true" + span + | Deployment of + span#discovery-pending-cloud-jobs-count<> + | more + span.singular.hidden<> node is + span.plural.hidden<> nodes are + | pending... + +.alert.alert-danger#discovery-bootstrap-alert.hidden role="alert" + i.fa.fa-4x.pull-left aria-hidden="true" + span + span#discovery-cloud-job-errors-count<> + | node + span.singular.hidden<> deployment + span.plural.hidden<> deployments + | failed. Please check + code<> + | /var/log/salt/minion + | for details. + .alert.alert-warning.discovery-minimum-nodes-alert role="alert" hidden="true" i.fa.fa-4x.pull-left aria-hidden="true" span diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/bin/init new/velum-master/bin/init --- old/velum-master/bin/init 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/bin/init 2018-05-09 20:02:40.000000000 +0200 @@ -1,19 +1,9 @@ #!/bin/bash -# This script will setup the database and then start the rails application +# This script will setup the database set -e -: ${VELUM_PORT:=80} - -setup_root_ca() { - # Velum is going to need this CA to talk to the running CaaSP Cluster - [ -f "/etc/pki/trust/anchors/SUSE_CaaSP_CA.pem" ] && return - - cp /etc/pki/ca.crt /etc/pki/trust/anchors/SUSE_CaaSP_CA.pem - update-ca-certificates -} - setup_database() { set +e @@ -75,8 +65,6 @@ fi } -setup_root_ca setup_database setup_pillar_seeds setup_cpi -bundle exec "puma -C config/puma.rb" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/bin/run new/velum-master/bin/run --- old/velum-master/bin/run 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/bin/run 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,12 @@ +#!/bin/bash + +setup_root_ca() { + # Velum is going to need this CA to talk to the running CaaSP Cluster + [ -f "/etc/pki/trust/anchors/SUSE_CaaSP_CA.pem" ] && return + + cp /etc/pki/ca.crt /etc/pki/trust/anchors/SUSE_CaaSP_CA.pem + update-ca-certificates +} + +setup_root_ca +bundle exec "puma -C config/puma.rb" \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/db/migrate/20180427014552_create_salt_jobs.rb new/velum-master/db/migrate/20180427014552_create_salt_jobs.rb --- old/velum-master/db/migrate/20180427014552_create_salt_jobs.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/db/migrate/20180427014552_create_salt_jobs.rb 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,13 @@ +class CreateSaltJobs < ActiveRecord::Migration + def change + create_table :salt_jobs do |t| + t.string :jid + t.integer :retcode + t.text :master_trace + t.text :minion_trace + + t.timestamps null: false + end + add_index :salt_jobs, :jid + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/db/schema.rb new/velum-master/db/schema.rb --- old/velum-master/db/schema.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/db/schema.rb 2018-05-09 20:02:40.000000000 +0200 @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180406080400) do +ActiveRecord::Schema.define(version: 20180427014552) do create_table "certificate_services", force: :cascade do |t| t.integer "certificate_id", limit: 4 @@ -106,6 +106,17 @@ add_index "salt_events", ["tag"], name: "tag", using: :btree add_index "salt_events", ["worker_id", "taken_at"], name: "index_salt_events_on_worker_id_and_taken_at", using: :btree + create_table "salt_jobs", force: :cascade do |t| + t.string "jid", limit: 255 + t.integer "retcode", limit: 4 + t.text "master_trace", limit: 65535 + t.text "minion_trace", limit: 65535 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "salt_jobs", ["jid"], name: "index_salt_jobs_on_jid", using: :btree + create_table "salt_returns", id: false, force: :cascade do |t| t.string "fun", limit: 50, null: false t.string "jid", limit: 255, null: false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/packaging/suse/patches/0_set_default_salt_events_alter_time_column_value.rpm.patch new/velum-master/packaging/suse/patches/0_set_default_salt_events_alter_time_column_value.rpm.patch --- old/velum-master/packaging/suse/patches/0_set_default_salt_events_alter_time_column_value.rpm.patch 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/packaging/suse/patches/0_set_default_salt_events_alter_time_column_value.rpm.patch 2018-05-09 20:02:40.000000000 +0200 @@ -2,7 +2,7 @@ index b8392cd..6061543 100644 --- a/db/schema.rb +++ b/db/schema.rb -@@ -95,7 +95,7 @@ ActiveRecord::Schema.define(version: 20180406080400) do +@@ -95,7 +95,7 @@ ActiveRecord::Schema.define(version: 20180427014552) do create_table "salt_events", force: :cascade do |t| t.string "tag", limit: 255, null: false t.text "data", limit: 16777215, null: false @@ -11,12 +11,12 @@ t.string "master_id", limit: 255, null: false t.datetime "taken_at" t.datetime "processed_at" -@@ -113,7 +113,7 @@ ActiveRecord::Schema.define(version: 20180406080400) do +@@ -113,7 +113,7 @@ ActiveRecord::Schema.define(version: 20180427014552) do t.string "id", limit: 255, null: false t.string "success", limit: 10, null: false t.text "full_ret", limit: 16777215, null: false - t.datetime "alter_time", null: false + t.column "alter_time", "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" end - + add_index "salt_returns", ["fun"], name: "fun", using: :btree diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/controllers/setup_controller_spec.rb new/velum-master/spec/controllers/setup_controller_spec.rb --- old/velum-master/spec/controllers/setup_controller_spec.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/spec/controllers/setup_controller_spec.rb 2018-05-09 20:02:40.000000000 +0200 @@ -674,6 +674,25 @@ get :discovery expect(response.status).to eq 200 end + + describe "as JSON" do + let(:pending) { 3 } + let(:failed) { 1 } + + before do + pending.times { FactoryGirl.create(:salt_job) } + failed.times { FactoryGirl.create(:salt_job_failed) } + get :discovery, format: :json + end + + it "includes a count of incomplete cloud jobs" do + expect(JSON.parse(response.body)["pending_cloud_jobs"]).to eq(pending) + end + + it "includes a count of failed cloud jobs" do + expect(JSON.parse(response.body)["cloud_jobs_failed"]).to eq(failed) + end + end end describe "POST /setup/bootstrap" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/factories/salt_job_factory.rb new/velum-master/spec/factories/salt_job_factory.rb --- old/velum-master/spec/factories/salt_job_factory.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/spec/factories/salt_job_factory.rb 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :salt_job do + jid { Time.current.strftime("%Y%m%d%H%M%S%6NS") } + + factory :salt_job_failed do + retcode 1 + master_trace { FFaker::Lorem.sentence } + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/features/bootstrap_cluster_feature_spec.rb new/velum-master/spec/features/bootstrap_cluster_feature_spec.rb --- old/velum-master/spec/features/bootstrap_cluster_feature_spec.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/spec/features/bootstrap_cluster_feature_spec.rb 2018-05-09 20:02:40.000000000 +0200 @@ -20,11 +20,24 @@ { minion_id: SecureRandom.hex, fqdn: "minion4.k8s.local" }] end + let(:pending) { 5 } + let(:failed) { 2 } + before do + pending.times { FactoryGirl.create(:salt_job) } + failed.times { FactoryGirl.create(:salt_job_failed) } allow_any_instance_of(Velum::SaltMinion).to receive(:assign_role).and_return(true) allow(Orchestration).to receive(:run) end + it "Includes a count of pending cloud jobs", js: true do + expect(page).to have_content("Deployment of #{pending} more nodes are pending...") + end + + it "Include a notice of failed cloud jobs", js: true do + expect(page).to have_content("#{failed} node deployments failed.") + end + it "A user sees warning modal when trying to bootstrap 2 nodes", js: true do # select master minion0.k8s.local find(".minion_#{minions[0].id} .master-btn").click diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/models/cloud_cluster_spec.rb new/velum-master/spec/models/cloud_cluster_spec.rb --- old/velum-master/spec/models/cloud_cluster_spec.rb 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/spec/models/cloud_cluster_spec.rb 2018-05-09 20:02:40.000000000 +0200 @@ -218,4 +218,50 @@ expect(cluster.to_s).to match(substring) end end + + context "when building" do + let(:cluster) do + described_class.new( + instance_type: custom_instance_type, + instance_count: instance_count, + resource_group: resource_group, + network_id: network_id, + subnet_id: subnet_id, + security_group_id: security_group_id + ) + end + let(:error_message) { "[\"Nope!\"]" } + + it "captures jobs" do + VCR.use_cassette("salt/cloud_profile", record: :none, allow_playback_repeats: true) do + cluster.save! + cluster.build! + expect(SaltJob.all_open.count).to eq(instance_count) + end + end + + it "captures errors" do + VCR.use_cassette("salt/login_500", record: :none, allow_playback_repeats: true) do + cluster.save! + cluster.build! + expect(cluster.errors[:base]).to include(error_message) + end + end + + context "with prior failed jobs" do + let(:failures) { 3 } + + before do + failures.times { FactoryGirl.create(:salt_job_failed) } + end + + it "clears failed jobs when (re)starting" do + VCR.use_cassette("salt/cloud_profile", record: :none, allow_playback_repeats: true) do + cluster.save! + cluster.build! + expect(SaltJob.failed.count).to be_zero + end + end + end + end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/models/salt_handler/cloud_bootstrap_spec.rb new/velum-master/spec/models/salt_handler/cloud_bootstrap_spec.rb --- old/velum-master/spec/models/salt_handler/cloud_bootstrap_spec.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/spec/models/salt_handler/cloud_bootstrap_spec.rb 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,213 @@ +require "rails_helper" + +describe SaltHandler::CloudBootstrap do + let!(:jid) { Time.current.strftime("%Y%m%d%H%M%S%6N") } + let(:node_id) { "caasp-node-" + SecureRandom.hex(8) } + let(:ip) { FFaker::Internet.ip_v4_address } + let(:instance_id) { "i-" + SecureRandom.hex(17) } + let!(:job) { FactoryGirl.create(:salt_job, jid: jid) } + let(:salt_event) do + event_data = { + "fun_args" => ["cluster_node", node_id], + "jid" => jid, + "return" => { + node_id => { + "productCodes" => nil, + "vpcId" => "vpc-" + SecureRandom.hex(8), + "instanceId" => instance_id, + "image" => "ami-" + SecureRandom.hex(8), + "imageId" => "ami-" + SecureRandom.hex(8), + "keyName" => "caasp-ip-" + ip.tr(".", "-"), + "clientToken" => nil, + "subnetId" => "subnet-" + SecureRandom.hex(8), + "amiLaunchIndex" => "0", + "instanceType" => "t2.xlarge", + "size" => "t2.xlarge", + "groupSet" => { + "item" => { + "groupName" => "caasp-ip-" + ip.tr(".", "-"), + "groupId" => "sg-" + SecureRandom.hex(8) + } + }, + "monitoring" => { "state" => "disabled" }, + "id" => instance_id, + "state" => "running", + "dnsName" => nil, + "privateIpAddress" => ip, + "virtualizationType" => "hvm", + "privateDnsName" => "ip-" + ip.tr(".", "-") + ".us-west-2.compute.internal", + "reason" => nil, + "tagSet" => { + "item" => { "key" => "Name", "value" => node_id } + }, + "deployed" => true, + "private_ips" => ip, + "sourceDestCheck" => "true", + "blockDeviceMapping" => { + "item" => { + "deviceName" => "/dev/sda1", + "ebs" => { + "status" => "attached", + "deleteOnTermination" => "true", + "volumeId" => "vol-" + SecureRandom.hex(17), + "attachTime" => FFaker::Time.datetime + } + } + }, + "placement" => { + "groupName" => nil, + "tenancy" => "default", + "availabilityZone" => "us-west-2c" + }, + "name" => node_id, + "instanceState" => { "code" => "16", "name" => "running" }, + "networkInterfaceSet" => { + "item" => { + "status" => "in-use", + "macAddress" => FFaker::Internet.mac, + "sourceDestCheck" => "true", + "vpcId" => "vpc-" + SecureRandom.hex(8), + "description" => nil, + "networkInterfaceId" => "eni-" + SecureRandom.hex(8), + "privateIpAddress" => ip, + "groupSet" => { + "item" => { + "groupName" => "caasp-ip-" + ip.tr(".", "-"), + "groupId" => "sg-" + SecureRandom.hex(8) + } + }, + "attachment" => { + "status" => "attached", + "deviceIndex" => "0", + "deleteOnTermination" => "true", + "attachmentId" => "eni-attach-" + SecureRandom.hex(8), + "attachTime" => FFaker::Time.datetime + }, + "subnetId" => "subnet-" + SecureRandom.hex(8), + "ownerId" => FFaker::PhoneNumber.imei, + "privateIpAddressesSet" => { + "item" => { + "privateIpAddress" => ip, + "primary" => "true", + "association" => { + "publicIp" => FFaker::Internet.ip_v4_address, + "publicDnsName" => nil, + "ipOwnerId" => "amazon" + } + } + }, + "association" => { + "publicIp" => FFaker::Internet.ip_v4_address, + "publicDnsName" => nil, + "ipOwnerId" => "amazon" + } + } + }, + "public_ips" => FFaker::Internet.ip_v4_address, + "ebsOptimized" => "false", + "launchTime" => FFaker::Time.datetime, + "architecture" => "x86_64", + "hypervisor" => "xen", + "rootDeviceType" => "ebs", + "ipAddress" => FFaker::Internet.ip_v4_address, + "rootDeviceName" => "/dev/sda1" + } + }, + "retcode" => 0, + "success" => true, + "cmd" => "_return", + "_stamp" => FFaker::Time.datetime, + "fun" => "cloud.profile", + "id" => "admin" + }.to_json + + FactoryGirl.create(:salt_event, + tag: "salt/job/" + jid + "/ret/admin", + data: event_data) + end + + let(:failed_salt_event) do + return_string = <<-RETURN + The minion function caused an exception: Traceback (most recent call last): + File "/usr/lib/python2.7/site-packages/salt/minion.py", line 1455, in _thread_return + return_data = executor.execute() + File "/usr/lib/python2.7/site-packages/salt/executors/direct_call.py", line 28, in execute + return self.func(*self.args, **self.kwargs) + File "/usr/lib/python2.7/site-packages/salt/modules/cloud.py", line 199, in profile_ + info = client.profile(profile, names, vm_overrides=vm_overrides, **kwargs) + File "/usr/lib/python2.7/site-packages/salt/cloud/__init__.py", line 352, in profile mapper.run_profile(profile, names, vm_overrides=vm_overrides) + File "/usr/lib/python2.7/site-packages/salt/cloud/__init__.py", line 1465, in run_profile + raise SaltCloudSystemExit('Failed to deploy VM') + SaltCloudSystemExit: Failed to deploy VM + RETURN + + event_data = { + "fun_args" => ["cluster_node", node_id], + "jid" => "20180501164423788496", + "return" => return_string, + "success" => false, + "cmd" => "_return", + "_stamp" => FFaker::Time.datetime, + "fun" => "cloud.profile", + "id" => "admin", + "out" => "nested" + }.to_json + + FactoryGirl.create(:salt_event, + tag: "salt/job/" + jid + "/ret/admin", + data: event_data) + end + + describe "when handling events" do + let(:random_event) do + FactoryGirl.create(:salt_event, + tag: "salt/job/" + jid + "/ret/" + jid, + data: {}.to_json) + end + let(:admin_other_event) do + FactoryGirl.create(:salt_event, + tag: "salt/job/" + jid + "/ret/admin", + data: { "fun" => "foo.bar" }.to_json) + end + let(:untracked_event) do + FactoryGirl.create(:salt_event, + tag: "salt/job/1/ret/admin", + data: { "fun" => "cloud.profile" }.to_json) + end + + it "returns false if job was not run on admin node" do + expect(described_class).not_to be_can_handle_event(random_event) + end + + it "returns false if job is not using 'cloud.profile' function" do + expect(described_class).not_to be_can_handle_event(admin_other_event) + end + + it "returns failse if job id is not tracked" do + expect(described_class).not_to be_can_handle_event(untracked_event) + end + + it "returns true if admin node job, 'cloud.profile' function, tracked job id" do + expect(described_class).to be_can_handle_event(salt_event) + end + end + + describe "when processing events" do + let(:handler) { described_class.new(salt_event) } + let(:failed_handler) { described_class.new(failed_salt_event) } + + it "updates the job on a successful event" do + handler.process_event + job.reload + expect(job).to be_completed + expect(job).to be_succeeded + end + + it "updates the job on a failed event" do + failed_handler.process_event + job.reload + expect(job).to be_completed + expect(job).to be_failed + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/models/salt_job_spec.rb new/velum-master/spec/models/salt_job_spec.rb --- old/velum-master/spec/models/salt_job_spec.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/spec/models/salt_job_spec.rb 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,104 @@ +require "rails_helper" + +describe SaltJob do + it { is_expected.to validate_uniqueness_of(:jid) } + + context "when a job is completed" do + it "defaults to falsey" do + expect(described_class.new).not_to be_completed + end + + it "can be set by an action" do + job = described_class.new + job.complete! + expect(job).to be_completed + end + end + + context "when storing the job return code" do + it "defaults to success" do + job = described_class.new + job.complete! + expect(job.retcode).to eq(0) + end + + it "can be set for success during completion" do + job = described_class.new + job.complete!(0) + expect(job.retcode).to eq(0) + end + + it "can be set for failure" do + job = described_class.new + job.complete!(1) + expect(job.retcode).to eq(1) + end + + it "informs success" do + job = described_class.new + job.complete!(0) + expect(job).to be_succeeded + end + + it "informs against failure" do + job = described_class.new + job.complete!(0) + expect(job).not_to be_failed + end + + it "informs failure" do + job = described_class.new + job.complete!(1) + expect(job).to be_failed + end + + it "informs against success" do + job = described_class.new + job.complete!(1) + expect(job).not_to be_succeeded + end + end + + context "when storing error traces" do + let(:master_trace) { FFaker::Lorem.paragraph } + let(:minion_trace) { FFaker::Lorem.paragraph } + + it "is empty on success" do + job = described_class.new + job.complete! + expect(job.master_trace).to be_nil + expect(job.minion_trace).to be_nil + end + + it "set when the job is completed" do + job = described_class.new + job.complete!(1, master_trace: master_trace, minion_trace: minion_trace) + expect(job.master_trace).to eq(master_trace) + expect(job.minion_trace).to eq(minion_trace) + end + + context "when evaluating the error trace" do + let(:jid) { Time.current.strftime("%Y%m%d%H%M%S%6N") } + let(:log_reference_msg) do + "Please check `/var/log/salt/minion` for details." + end + let(:upstream_error_msg) do + "InstanceLimitExceeded: "\ + "Your quota allows for 0 more running instance(s). "\ + "You requested at least 1" + end + + it "points at the logs with only a master trace" do + job = described_class.new(jid: jid) + job.complete!(1, master_trace: master_trace) + expect(job.errors[:base]).to include(log_reference_msg) + end + + # it "provides an upstream error with a minion trace" do + # job = described_class.new(jid: jid) + # job.complete!(1, minion_trace: minion_trace) + # expect(job.errors[:base]).to include(upstream_error_msg) + # end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/vcr_cassettes/salt/cloud_profile.yml new/velum-master/spec/vcr_cassettes/salt/cloud_profile.yml --- old/velum-master/spec/vcr_cassettes/salt/cloud_profile.yml 2018-05-09 09:05:11.000000000 +0200 +++ new/velum-master/spec/vcr_cassettes/salt/cloud_profile.yml 2018-05-09 20:02:40.000000000 +0200 @@ -101,7 +101,267 @@ 06:57:34 GMT; Path=/ body: encoding: UTF-8 - string: '{"return": [{"jid": "20180205205734878026", "minions": ["admin"]}]}' + string: '{"return": [{"jid": "20180501142133027777", "minions": ["admin"]}]}' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +- request: + method: post + uri: https://127.0.0.1:8000/ + body: + encoding: UTF-8 + string: '{"client":"local_async","tgt":"admin","fun":"cloud.profile","arg":["cluster_node","caasp-node-d43efb76"]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + response: + status: + code: 200 + message: OK + headers: + Content-Length: + - '67' + Access-Control-Expose-Headers: + - GET, POST + Cache-Control: + - private + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '{"return": [{"jid": "20180501142133027836", "minions": ["admin"]}]}' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +- request: + method: post + uri: https://127.0.0.1:8000/ + body: + encoding: UTF-8 + string: '{"client":"local_async","tgt":"admin","fun":"cloud.profile","arg":["cluster_node","caasp-node-d43efb76"]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + response: + status: + code: 200 + message: OK + headers: + Content-Length: + - '67' + Access-Control-Expose-Headers: + - GET, POST + Cache-Control: + - private + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '{"return": [{"jid": "20180501142133027856", "minions": ["admin"]}]}' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +- request: + method: post + uri: https://127.0.0.1:8000/ + body: + encoding: UTF-8 + string: '{"client":"local_async","tgt":"admin","fun":"cloud.profile","arg":["cluster_node","caasp-node-d43efb76"]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + response: + status: + code: 200 + message: OK + headers: + Content-Length: + - '67' + Access-Control-Expose-Headers: + - GET, POST + Cache-Control: + - private + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '{"return": [{"jid": "20180501142133027871", "minions": ["admin"]}]}' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +- request: + method: post + uri: https://127.0.0.1:8000/ + body: + encoding: UTF-8 + string: '{"client":"local_async","tgt":"admin","fun":"cloud.profile","arg":["cluster_node","caasp-node-d43efb76"]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + response: + status: + code: 200 + message: OK + headers: + Content-Length: + - '67' + Access-Control-Expose-Headers: + - GET, POST + Cache-Control: + - private + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '{"return": [{"jid": "20180501142133027885", "minions": ["admin"]}]}' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +- request: + method: post + uri: https://127.0.0.1:8000/ + body: + encoding: UTF-8 + string: '{"client":"local_async","tgt":"admin","fun":"cloud.profile","arg":["cluster_node","caasp-node-d43efb76"]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + response: + status: + code: 200 + message: OK + headers: + Content-Length: + - '67' + Access-Control-Expose-Headers: + - GET, POST + Cache-Control: + - private + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '{"return": [{"jid": "20180501142133027976", "minions": ["admin"]}]}' http_version: recorded_at: Mon, 05 Feb 2018 20:57:34 GMT recorded_with: VCR 3.0.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/velum-master/spec/vcr_cassettes/salt/login_500.yml new/velum-master/spec/vcr_cassettes/salt/login_500.yml --- old/velum-master/spec/vcr_cassettes/salt/login_500.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/velum-master/spec/vcr_cassettes/salt/login_500.yml 2018-05-09 20:02:40.000000000 +0200 @@ -0,0 +1,103 @@ +--- +http_interactions: +- request: + method: post + uri: https://127.0.0.1:8000/login + body: + encoding: UTF-8 + string: '{"username":"saltapi","password":"l+ZtDm9lG1DPdt/QyFfABgWtCN/IKwnmGTK8nCt++PiOVG9Y2NccIrozchvz7RtxREIZe5CshcO0","eauth":"pam"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + response: + status: + code: 500 + message: Internal Server Error + headers: + Content-Length: + - 9 + Access-Control-Expose-Headers: + - GET, POST + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '["Nope!"]' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +- request: + method: post + uri: https://127.0.0.1:8000/ + body: + encoding: UTF-8 + string: '{"username":"saltapi","password":"l+ZtDm9lG1DPdt/QyFfABgWtCN/IKwnmGTK8nCt++PiOVG9Y2NccIrozchvz7RtxREIZe5CshcO0","eauth":"pam"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json; charset=utf-8 + User-Agent: + - Ruby + Host: + - 127.0.0.1:8000 + Content-Type: + - application/json; charset=utf-8 + response: + status: + code: 500 + message: Internal Server Error + headers: + Content-Length: + - 9 + Access-Control-Expose-Headers: + - GET, POST + Vary: + - Accept-Encoding + Server: + - CherryPy/3.6.0 + Allow: + - GET, HEAD, POST + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 05 Feb 2018 20:57:34 GMT + Access-Control-Allow-Origin: + - "*" + X-Auth-Token: + - 507fef45a35e7038c9a1cdb754acfc7539aac4a2 + Content-Type: + - application/json + Set-Cookie: + - session_id=507fef45a35e7038c9a1cdb754acfc7539aac4a2; expires=Tue, 06 Feb 2018 + 06:57:34 GMT; Path=/ + body: + encoding: UTF-8 + string: '["Nope!"]' + http_version: + recorded_at: Mon, 05 Feb 2018 20:57:34 GMT +recorded_with: VCR 3.0.3
