diff --git a/lib/bushslicer.rb b/lib/bushslicer.rb index f4b5b0c5bf..ed198a95b5 100644 --- a/lib/bushslicer.rb +++ b/lib/bushslicer.rb @@ -16,6 +16,7 @@ module BushSlicer autoload :OpenStack, "launchers/openstack" autoload :VSphere, "launchers/v_sphere" autoload :Packet, "launchers/packet" + autoload :OCM, "launchers/ocm" autoload :EnvironmentLauncher, "launchers/environment_launcher" autoload :PolarShift, "polarshift/autoload" diff --git a/lib/launchers/amz.rb b/lib/launchers/amz.rb index e3caf1abad..20a6773f57 100755 --- a/lib/launchers/amz.rb +++ b/lib/launchers/amz.rb @@ -52,6 +52,8 @@ def initialize(access_key: nil, secret_key: nil, service_name: nil, region: nil) ) }) ) Aws.config.update( config[:config_opts].merge({region: region})) if region + + @account_id = awscred["aws_account_id"] end private def client_ec2 @@ -615,6 +617,11 @@ def secret_key ec2.client.config.credentials.secret_access_key end + # @return [String] + def account_id + return @account_id + end + # @return [Object] undefined def terminate_instance(instance) # we don't really have root permission to terminate, we'll just label it diff --git a/lib/launchers/cloud_helper.rb b/lib/launchers/cloud_helper.rb index 03167dde55..f4c307c603 100644 --- a/lib/launchers/cloud_helper.rb +++ b/lib/launchers/cloud_helper.rb @@ -39,6 +39,8 @@ def iaas_by_service(service_name) BushSlicer::Alicloud.new(service_name: service_name) when "packet" BushSlicer::Packet.new(service_name: service_name) + when "ocm" + BushSlicer::OCM.new(service_name: service_name) else raise "unknown service type " \ "#{conf[:services, service_name, :cloud_type]} for cloud " \ diff --git a/lib/launchers/ocm.rb b/lib/launchers/ocm.rb new file mode 100644 index 0000000000..edd09bee45 --- /dev/null +++ b/lib/launchers/ocm.rb @@ -0,0 +1,200 @@ +#!/usr/bin/env ruby + +lib_path = File.expand_path(File.dirname(File.dirname(__FILE__))) +unless $LOAD_PATH.any? {|p| File.expand_path(p) == lib_path} + $LOAD_PATH.unshift(lib_path) +end + +require 'common' +require 'json' +require 'tmpdir' +require 'git' + +module BushSlicer + class OCM + include Common::Helper + + attr_reader :config + attr_reader :token, :token_file, :url, :region, :version, :nodes, :lifespan, :cloud, :cloud_opts, :multi_az + + def initialize(**options) + service_name = ENV['OCM_SERVICE_NAME'] || options[:service_name] || 'ocm' + @opts = default_opts(service_name)&.merge options + unless @opts + @opts = options + end + + # OCM token is mandatory + # it can be defined by token or by token_file + @token = ENV['OCM_TOKEN'] || @opts[:token] + @token_file = @opts[:token_file] + unless @token + if @token_file + token_file_path = expand_private_path(@token_file) + @token = File.read(token_file_path) + else + raise "You need to specify OCM token by 'token' or by 'token_file'" + end + end + + # region is mandatory + # in the future we can extend support for other clouds, e.g. GCP and ARO + @region = ENV['OCM_REGION'] || ENV['AWS_REGION'] || @opts[:region] + + # url defines the OCM environment (prod, integration or stage) + # currently, the url is ignored as many teams use the stage environment + @url = ENV['OCM_URL'] || @opts[:url] || 'https://api.stage.openshift.com' + + # openshift version is optional + @version = ENV['OCM_VERSION'] || ENV['OCP_VERSION'] || @opts[:version] + + # number of worker nodes + # minimum is 2 + # default value is 4 + @nodes = ENV['OCM_NODES'] || @opts[:nodes] + + # lifespan in hours + # default value is 24 hours + @lifespan = ENV['OCM_LIFESPAN'] || @opts[:lifespan] + + # multi_az is optional + # default value is false + @multi_az = ENV['OCM_MULTI_AZ'] || @opts[:multi_az] + + # BYOC (Bring Your Own Cloud) + # you can refer to already defined cloud in config.yaml + # currently, only AWS is supported + @cloud = ENV['OCM_CLOUD'] || @opts[:cloud] + if @cloud + @cloud_opts = default_opts(@cloud) + unless @cloud_opts + raise "Cannot find cloud '#{cloud}' defined in '#{service_name}'" + end + end + end + + # @param service_name [String] the service name of this openstack instance + # to lookup in configuration + def default_opts(service_name) + return conf[:services, service_name.to_sym] + end + + def to_seconds(string) + regex_m = /^(\d+)\s*(m|min|minutes|mins)+$/ + regex_h = /^(\d+)\s*(h|hour|hours|hrs)+$/ + regex_d = /^(\d+)\s*(d|day|days)+$/ + regex_w = /^(\d+)\s*(w|week|weeks|wks)+$/ + case string + when regex_m + return string.match(regex_m)[1].to_i * 60 + when regex_h + return string.match(regex_h)[1].to_i * 60 * 60 + when regex_d + return string.match(regex_d)[1].to_i * 24 * 60 * 60 + when regex_w + return string.match(regex_w)[1].to_i * 7 * 24 * 60 * 60 + else + raise "Cannot convert '#{string}' to seconds!" + end + end + + # create a json which specifies OSD cluster + # in the future we plan to move the logic into the script 'osd-provision.sh' + def generate_json(name) + json_data = { + "name" => name, + "managed" => true, + "multi_az" => false, + "byoc" => false + } + + if @multi_az + json_data.merge!({"multi_az" => @multi_az}) + end + + if @region + json_data.merge!({"region" => {"id" => @region}}) + end + + if @version + json_data.merge!({"version" => {"id" => "openshift-v#{@version}"}}) + end + + if @nodes + json_data.merge!({"nodes" => {"compute" => @nodes.to_i}}) + end + + if @lifespan + expiration = Time.now + to_seconds(@lifespan) + json_data.merge!({"expiration_timestamp" => expiration.strftime("%Y-%m-%dT%H:%M:%SZ")}) + end + + if @cloud_opts + case @cloud_opts[:cloud_type] + when "aws" + aws = Amz_EC2.new(service_name: @cloud) + json_data.merge!({"aws" => {"access_key_id":aws.access_key, "secret_access_key":aws.secret_key, "account_id":aws.account_id}}) + json_data.merge!({"byoc" => true}) + end + end + + return json_data.to_json + end + + # download the script 'osd-provision.sh' which takes care of the OSD installation/uninstallation + def download_osd_script + osd_repo_uri = ENV['GIT_OSD_URI'] || 'https://gitlab.cee.redhat.com/mk-bin-packing/mk-performance-tests.git' + osd_repo_dir = File.join(Dir.tmpdir, 'osd_repo') + FileUtils.rm_rf(osd_repo_dir) + git = BushSlicer::Git.new(uri: osd_repo_uri, dir: osd_repo_dir) + git.clone + osd_script = File.join(osd_repo_dir, 'scripts', 'osd-provision.sh') + if !File.exists?(osd_script) + raise "Cannot find #{osd_script}" + end + return osd_script + end + + def shell(cmd, output = nil) + if output + res = Host.localhost.exec(cmd, single: true, stderr: :stdout, stdout: output, timeout: 3600) + else + res = Host.localhost.exec(cmd, single: true, timeout: 3600) + end + if res[:success] + return res[:response] + else + raise "Error when executing '#{cmd}'. Response: #{res[:response]}" + end + end + + # create OSD cluster + def create_osd(name) + # cerate a temp file with ocm-token + ocm_token_file = Tempfile.new("ocm-token-file", Host.localhost.workdir) + File.write(ocm_token_file, @token) + # create cluster.json in a workdir/install-dir + install_dir = File.join(Host.localhost.workdir, 'install-dir') + FileUtils.mkdir_p(install_dir) + ocm_json_file = File.join(install_dir, 'cluster.json') + File.write(ocm_json_file, generate_json(name)) + # now, download the script which will take care of the OSD cluster installation + osd_script = download_osd_script + shell("#{osd_script} --create --cloud-token-file #{ocm_token_file.path} -f #{ocm_json_file} --wait", STDOUT) + shell("#{osd_script} --get api_url -f #{ocm_json_file}") + shell("#{osd_script} --get credentials -f #{ocm_json_file}") + end + + # delete OSD cluster + def delete_osd(name) + # create a temp file with ocm-token + ocm_token_file = Tempfile.new("ocm-token-file", Host.localhost.workdir) + File.write(ocm_token_file, @token) + # now, download the script which will take care of the OSD cluster installation + osd_script = download_osd_script + shell("#{osd_script} --delete --cloud-token-file #{ocm_token_file.path} -n #{name}") + end + + end + +end diff --git a/lib/launchers/ocm_test.rb b/lib/launchers/ocm_test.rb new file mode 100644 index 0000000000..3861f31f2a --- /dev/null +++ b/lib/launchers/ocm_test.rb @@ -0,0 +1,96 @@ +ENV['BUSHSLICER_PRIVATE_DIR'] = nil +ENV['OCM_NAME'] = nil +ENV['OCM_TOKEN'] = nil +ENV['OCM_URL'] = nil +ENV['OCM_REGION'] = nil +ENV['OCM_VERSION'] = nil +ENV['OCM_LIFESPAN'] = nil + +lib_path = File.expand_path(File.dirname(File.dirname(__FILE__))) +unless $LOAD_PATH.any? {|p| File.expand_path(p) == lib_path} + $LOAD_PATH.unshift(lib_path) +end + +require 'fileutils' +require 'test/unit' +require_relative './ocm' + +class MyTest < Test::Unit::TestCase + def setup + + end + + # def teardown + # end + + def test_default_url + options = { :token => "abc" } + ocm = BushSlicer::OCM.new(options) + assert_equal('https://api.stage.openshift.com', ocm.url) + end + + def test_generating_json + options = { :token => "abc" } + ocm = BushSlicer::OCM.new(options) + json = ocm.generate_json('myosd4') + assert_equal('{"name":"myosd4","managed":true,"multi_az":false,"byoc":false}', json) + end + + def test_generating_json_with_region + options = { :token => "abc", :region => "us-east-1" } + ocm = BushSlicer::OCM.new(options) + json = ocm.generate_json('myosd4') + assert_equal('{"name":"myosd4","managed":true,"multi_az":false,"byoc":false,"region":{"id":"us-east-1"}}', json) + end + + def test_generating_json_with_version + options = { :token => "abc", :version => "4.6.1" } + ocm = BushSlicer::OCM.new(options) + json = ocm.generate_json('myosd4') + assert_equal('{"name":"myosd4","managed":true,"multi_az":false,"byoc":false,"version":{"id":"openshift-v4.6.1"}}', json) + end + + def test_generating_json_with_lifespan + options = { :token => "abc", :lifespan => "25h" } + ocm = BushSlicer::OCM.new(options) + json = ocm.generate_json('myosd4') + time = Time.now + 60 * 60 * 25 + year = time.strftime("%Y") + month = time.strftime("%m") + day = time.strftime("%d") + assert_match(/.*"expiration_timestamp":"#{year}-#{month}-#{day}T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]Z".*/, json) + end + + def test_generating_json_with_nodes + options = { :token => "abc", :nodes => "8" } + ocm = BushSlicer::OCM.new(options) + json = ocm.generate_json('myosd4') + assert_equal('{"name":"myosd4","managed":true,"multi_az":false,"byoc":false,"nodes":{"compute":8}}', json) + end + + def test_downloading_osd_script + options = { :token => "abc" } + ocm = BushSlicer::OCM.new(options) + osd_script = ocm.download_osd_script + assert(File.exists?(osd_script), "File 'osd-provision.sh' was not downloaded") + content = File.read(osd_script) + assert_match(/.*ocm.*/, content) + end + + def test_executing_shell + hello_script = "/tmp/hello.sh" + File.write(hello_script, "#!/bin/sh\n[[ -z \"$1\" ]] && echo \"Specify a name!\" && exit 1; for i in {1..3}; do echo \"Hello $1\"; sleep 5; done") + File.chmod(0755, hello_script) + options = { :token => "abc" } + ocm = BushSlicer::OCM.new(options) + result = ocm.shell("#{hello_script} World") + assert_equal("Hello World\nHello World\nHello World\n", result) + result = ocm.shell("#{hello_script} World", STDOUT) + assert_equal("", result) + error = assert_raises(RuntimeError) { ocm.shell("#{hello_script} ") } + assert_equal("Error when executing '#{hello_script} '. Response: Specify a name!\n", error.message) + error = assert_raises(RuntimeError) { ocm.shell("#{hello_script} ", STDOUT) } + assert_equal("Error when executing '#{hello_script} '. Response: ", error.message) + end + +end