How to use error method of Inspec Package

Best Inspec_ruby code snippet using Inspec.error

helpers.rb

Source:helpers.rb Github

copy

Full Screen

...25 end26 begin27 @cache_server = inspec.registry_key('HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion')['ProductName'].match? 'Server'28 rescue StandardError => e29 Inspec::Log.error('(server?) rescue:' + e.message)30 return false31 end32 Inspec::Log.debug('(server?) ending')33 @cache_server34end35# SALTSTACK HELPERS36require 'safe_yaml'37require 'train'38require 'timeout'39require "inspec/log"40require 'json'41require 'os'42SafeYAML::OPTIONS[:default_mode] = :safe43# https://www.inspec.io/docs/reference/inputs/44def set_input_pillar_default45 input('pillar', value: get_pillar_default, type: 'hash', description: 'SaltStack Pillar Data')46end47def get_pillar48 if defined?(@cache_pillar)49 Inspec::Log.debug('(get_pillar?) returning cached value')50 return @cache_pillar51 end52 @cache_pillar = try_get_pillar53 if defined?(@cache_pillar)54 Inspec::Log.debug('(get_pillar) success, got pillar. Cached result.')55 @cache_pillar56 end57end58def try_get_pillar59 # require 'pp'; pp input('pillar').diagnostic_string;60 # REMOVE THIS61 # return get_pillar_from_inspec_pillar_file62 unless input('pillar').is_a?(Inspec::Input::NO_VALUE_SET)63 Inspec::Log.debug('Got pillar from kitchen input.')64 puts 'INFO: Got pillar from kitchen input.'65 return input('pillar')66 end67 # pillar_from_minion = get_pillar_from_minion68 pillar_from_minion = ingest_from_minion('yaml', 'c:\salt\salt-call.bat --config-dir=C:\Users\vagrant\AppData\Local\Temp\kitchen\etc\salt pillar.items --retcode-passthrough | Select-String -Pattern "----------" -NotMatch')69 unless !defined?(pillar_from_minion) || pillar_from_minion == []70 Inspec::Log.debug('Got pillar from the target minion using WinRM.')71 puts 'INFO: Got pillar from the target minion using WinRM.'72 return pillar_from_minion['local']73 end74 pillar_from_inspec_pillar_file = get_pillar_from_inspec_pillar_file75 unless !defined?(pillar_from_inspec_pillar_file) || pillar_from_inspec_pillar_file == []76 Inspec::Log.debug('Got pillar from the inspec pillar file.')77 puts 'INFO: Got pillar from the inspec pillar file.'78 return pillar_from_inspec_pillar_file79 end80 raise 'Unable to get pillar from input, minion or local inspec pillar file.'81end82def get_pillar_from_inspec_pillar_file83 if defined?(@cache_pillar_from_inspec_pillar_file)84 Inspec::Log.debug('[get_pillar_from_inspec_pillar_file] returning cached value')85 return @cache_pillar_from_inspec_pillar_file86 end87 pillar_file = ENV['INSPEC_TEST_SALT_PILLAR'] || 'pillar.example'88 begin89 @cache_pillar_from_inspec_pillar_file = YAML.safe_load(File.read(pillar_file)) if File.exist?(pillar_file)90 rescue StandardError => e91 Inspec::Log.warn('[get_pillar_from_inspec_pillar_file] ' + e.message)92 return []93 end94 if defined?(@cache_pillar_from_inspec_pillar_file)95 Inspec::Log.debug('[get_pillar_from_inspec_pillar_file] success, got the pillar from the inspec file. Cached result.')96 @cache_pillar_from_inspec_pillar_file97 end98end99def ingest_from_minion(type, ps_cmd, max_retries = 20, sec_timeout = 10)100 # grep "WinRM address:" $(ls -t .kitchen/logs/*.log | head -n2 | tail -n1) | sed 's/^.*: //'101 # Test port open: nc -z -w1 localhost 55985;echo $?102 # nc -z -w1 $(sed -n -e 's/^.*WinRM address: //p' $(ls -t .kitchen/logs/*.log | sed -n '2p') | cut -f1 -d:) $(sed -n -e 's/^.*WinRM address: //p' $(ls -t .kitchen/logs/*.log | sed -n '2p') | cut -f2 -d:); echo $? 103 # cd .kitchen/kitchen-vagrant/{instance name}; vagrant winrm --command whoami104 # cd .kitchen/kitchen-vagrant/{instance name}; vagrant winrm-config105 # cmd ="nc -z -w1 $(sed -n -e 's/^.*WinRM address: //p' $(ls -t .kitchen/logs/*.log | sed -n '2p') | cut -f1 -d:) $(sed -n -e 's/^.*WinRM address: //p' $(ls -t .kitchen/logs/*.log | sed -n '2p') | cut -f2 -d:); echo $?"106 retries ||= 0107 Inspec::Log.debug("Ingesting #{type} content using `#{ps_cmd}` with timout of #{sec_timeout} and a max of #{max_retries} retries.")108 # require 'pry'; binding.pry109 begin110 # https://www.rubydoc.info/gems/train/0.14.1/Train%2FTransports%2FLocal%2FConnection:run_command111 # Train.Plugins.Transport.Connection train.connection.run_command112 Timeout.timeout(sec_timeout) do113 @my_result = JSON.parse(backend::backend.run_command(ps_cmd).stdout) if type == 'json'114 @my_result =YAML.safe_load(backend::backend.run_command(ps_cmd).stdout) if type == 'yaml'115 end116 rescue => e117 Inspec::Log.debug("`#{e.message}`, target may be rebooting after highstate. Remaining retries: #{max_retries - retries}")118 puts("`#{e.message}`, target may be rebooting after highstate. Remaining retries: #{max_retries - retries}")119 if (retries += 1) < max_retries120 retry121 else122 begin123 backend::backend.run_command('whoami').stdout124 rescue => e125 msg = "Unable to get whoami from backend::backend.run_command: #{e.message}"126 puts(msg)127 Inspec::Log.debug(msg)128 end129 if OS.windows?130 pwsh_cmd = '$test_path=".kitchen/kitchen-vagrant/$(Get-ChildItem -Path .kitchen/logs/*.log | Where-Object {$_.Name -ne "kitchen.log"} | Sort-Object -Property @{Expression = {$_.LastWriteTime}; Descending = $True} | Select-Object -Property BaseName -First 1 -expandproperty BaseName)"; Set-Location -Path $test_path; $test_vagrantfile = "$test_path/Vagrantfile"; Set-Content -Path $test_vagrantfile -Value (get-content -Path $test_vagrantfile | Select-String -Pattern "vagrant_vb_guest.rb" -NotMatch); Set-Location -Path $test_path; vagrant winrm'131 cmd = "powershell -command '#{pwsh_cmd}'"132 else133 cmd = "cd .kitchen/kitchen-vagrant/$(ls -t .kitchen/logs/*.log | grep -v .kitchen/logs/kitchen.log | head -n1 | cut -f3 -d/ | awk -F. '{print $1}'); vagrant winrm"134 end135 if system( cmd )136 puts('Successfully connected via `vagrant winrm`')137 Inspec::Log.debug('Successfully connected via `vagrant winrm`')138 else139 msg = 'Failed to connect via `vagrant winrm`'140 puts(msg)141 Inspec::Log.debug(msg)142 # abort msg143 end144 end145 end146 if defined?(@my_result)147 Inspec::Log.debug("Ingested #{type} content successfully from minion using Train.Plugins.Transport.Connection.")148 @my_result149 else150 Inspec::Log.debug('Failed to get content from minion using Train.Plugins.Transport.Connection.')151 puts "WARNING: Failed to get content from minion using Train.Plugins.Transport.Connection."152 []153 end154end155# pp get_mystates_from_highstate('module','chocolatey','bootstrap')156# pp get_mystates_from_highstate('module','user','current')157# pp get_mystates_from_highstate('state','system','hostname')158def get_highstate_from_minion159 if defined?(@cache_highstate_from_minion)160 Inspec::Log.debug('Returning cached @cache_highstate_from_minion.')161 return @cache_highstate_from_minion162 end163 # ingest_from_minion('yaml', 'C:\salt\salt-call.bat --config-dir=C:\Users\vagrant\AppData\Local\Temp\kitchen\etc\salt state.show_highstate --out yaml |Select-String -Pattern "__sls__", "__env__", "- run", "- order:" -NotMatch')164 highstate_from_minion = ingest_from_minion('json', 'C:\salt\salt-call.bat --config-dir=C:\Users\vagrant\AppData\Local\Temp\kitchen\etc\salt state.show_highstate --out json')165 if highstate_from_minion != []166 Inspec::Log.debug('Saving highstate to cache @cache_highstate_from_minion.')167 @cache_highstate_from_minion = highstate_from_minion['local']168 @cache_highstate_from_minion169 else170 Inspec::Log.error('Failed to get highstate.')171 abort 'Failed to get highstate.'172 end173end174# Example modules175# "windows.module.status.uptime"=>176# {"module"=>177# [{"status.uptime"=>[{"human_readable"=>true}]},178# {"require"=>["windows.module.user.current"]},179# "run",180# {"order"=>10004}],181# "__sls__"=>"windows.modules",182# "__env__"=>"base"},183# "chocolatey.bootstrap"=>184# {"module"=>185# [{"chocolatey.bootstrap"=>nil},186# {"unless"=>"where.exe chocolatey"},187# "run",188# {"order"=>10010}],189# "__sls__"=>"windows.system.packages.chocolatey.bootstrap",190# "__env__"=>"base"},191# Example State192# {"windows.state.system.computer_desc.description"=>193# {"system"=>194# [{"name"=>"Saltstack Computer Description"},195# {"require"=>["windows.state.system.hostname.saltstack1"]},196# "computer_desc",197# {"order"=>10000}],198# "__sls__"=>"windows.states",199# "__env__"=>"base"},200def get_mystates_from_highstate(type, find_state, find_function)201 found_states = {}202 highstate = get_highstate_from_minion203 Inspec::Log.debug("Getting #{type}s for #{find_state}.#{find_function}.")204 # puts "highstate class: " + highstate.class.to_s205 # pp highstate206 highstate.each do |state_id, state|207 # puts "state class: " + state.class.to_s208 Inspec::Log.debug("Checking #{state_id} from highstate.")209 case type210 when 'state'211 if state.key?(find_state) && state[find_state].include?(find_function)212 Inspec::Log.debug("Found state #{find_state}.#{find_function} from highstate.")213 found_states[state_id] = state214 end215 when 'module'216 if state.key?('module')217 check_module = state['module'][0]218 # pp check_module219 if check_module.key?("#{find_state}.#{find_function}") || (check_module.key?(find_state) && check_module[find_state][0] == find_function)220 Inspec::Log.debug("Found module #{find_state}.#{find_function} from highstate.")221 found_states[state_id] = state222 end223 end224 end225 end226 if found_states != {}227 Inspec::Log.debug("Got #{type}s for #{find_state}.#{find_function}.")228 # pp found_states229 found_states230 else231 Inspec::Log.error("Unable to get #{type}'s for #{find_state}.#{find_function}'")232 end233end234# example: get_saltstack_package_full_name('7zip') = '7-Zip'235def get_saltstack_package_full_name(package)236 # pillar = YAML.safe_load(File.read('test/salt/pillar/windows.sls'))237 url = 'https://raw.githubusercontent.com/saltstack/salt-winrepo-ng/master/'238 files = [package + '.sls', package + '/init.sls']239 # example: package = "7zip"=>{"version"=>"18.06.00.0", "refresh_minion_env_path"=>false}240 saltstack_package_full_name = files.find do |checkme|241 ps = "$f = (((Get-ChildItem -Path $env:LOCALAPPDATA -Filter 'salt-winrepo-ng' -Recurse -Directory).Fullname[0]) + '\\#{checkme.sub('/', '\\')}'); if (Test-Path $f -PathType Leaf) {Get-Content -Path $f}"242 begin243 file = (open(url + checkme) & :read)244 rescue245 begin...

Full Screen

Full Screen

archer.rb

Source:archer.rb Github

copy

Full Screen

1# encoding: utf-82require 'inspec/utils/filter'3require 'hashie/mash'4require 'inspec/resources/package'5# needed to skip cert verification when ssl_verify false6# this can be replaced with -SkipCertificateCheck in Powershell 6.x7SKIP_CERT_CHECK = %(8add-type @"9 using System.Net;10 using System.Security.Cryptography.X509Certificates;11 public class TrustAllCertsPolicy : ICertificatePolicy {12 public bool CheckValidationResult(13 ServicePoint srvPoint, X509Certificate certificate,14 WebRequest request, int certificateProblem) {15 return true;16 }17 }18"@19[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy20[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3, [Net.SecurityProtocolType]::Tls, [Net.SecurityProtocolType]::Tls11, [Net.SecurityProtocolType]::Tls1221)22LOGIN_PATH = '/api/core/security/login'.freeze23SECURITY_PARAMETERS_PATH = '/api/core/system/securityparameter'.freeze24class Archer < Inspec.resource(1)25 name 'archer'26 desc "Use the RSA Archer InSpec audit resource to test the status your RSA Archer system."27 example "28 describe archer(url: attribute('url'),29 instancename: attribute('instancename'),30 user_domain: attribute('user_domain'),31 username: attribute('username'),32 password: attribute('password'),33 ssl_verify: attribute('ssl_verify')) do34 its('default_administrative_user.MinPasswordLength') { should cmp >= 9 }35 its('general_user_parameter.MinPasswordLength') { should cmp >= 9 }36 its('archer_services_parameter.MinPasswordLength') { should cmp >= 9 }37 end38 "39 attr_reader40 def initialize(opts = {})41 unless inspec.command('curl').exist?42 raise Inspec::Exceptions::ResourceSkipped, "Package `curl` and `powershell` not avaiable on the host"43 end44 @url = opts.fetch(:url, 'https://localhost/')45 @creds = {}46 @creds['InstanceName'] = opts.fetch(:instancename, nil)47 @creds['Username'] = opts.fetch(:username, nil)48 @creds['UserDomain'] = opts.fetch(:userdomain, nil)49 @creds['Password'] = opts.fetch(:password, nil)50 @ssl_verify = opts.fetch(:ssl_verify, true)51 login if session_token.nil?52 end53 def to_s54 "Archer Instance #{@creds['InstanceName']}"55 end56 def default_administrative_user57 content = securityparameter[0]58 verify_json_payload!(content)59 Hashie::Mash.new(content['RequestedObject'])60 end61 def general_user_parameter62 content = securityparameter[1]63 verify_json_payload!(content)64 Hashie::Mash.new(content['RequestedObject'])65 end66 def archer_services_parameter67 content = securityparameter[2]68 verify_json_payload!(content)69 Hashie::Mash.new(content['RequestedObject'])70 end71 def curl_command(request: nil, headers: nil, body: nil, path: nil)72 cmd_string = ['curl']73 cmd_string << '-k' unless @ssl_verify74 cmd_string << "-X #{request}" unless request.nil?75 cmd_string << headers.map { |x, y| "-H \"#{x}\":\"#{y}\"" }.join(' ') unless headers.nil?76 cmd_string << "-H 'Content-Type: application/json'"77 cmd_string << "-d '#{body.to_json}'" unless body.nil?78 cmd_string << URI.join(@url, path)79 cmd_string.join(' ')80 end81 def invoke_restmethod_command(request: nil, headers: nil, body: nil, path: nil)82 cmd_string = ['Invoke-RestMethod']83 cmd_string << "-Method #{request}" unless request.nil?84 cmd_string << "-Headers @{#{headers.map { |x, y| " '#{x}' = '#{y}'" }.join(';')} }" unless headers.nil?85 cmd_string << "-ContentType 'application/json'"86 cmd_string << "-Body '#{body.to_json}'" unless body.nil?87 cmd_string << URI.join(@url, path)88 cmd_string << '| ConvertTo-Json'89 return "#{SKIP_CERT_CHECK} \n #{cmd_string.join(' ')}" unless @ssl_verify90 cmd_string.join(' ')91 end92 def parse_response(response)93 JSON.parse(response)94 rescue JSON::ParserError => e95 raise Inspec::Exceptions::ResourceSkipped, "Error Parsing JSON response from Archer: #{ e.message}."96 end97 def session_token=(token)98 ENV['INSPEC_ARCHER_SESSION_TOKEN'] = token99 end100 def session_token101 ENV['INSPEC_ARCHER_SESSION_TOKEN']102 end103 def login104 if inspec.os.windows?105 cmd = invoke_restmethod_command(request: 'POST',106 headers: nil,107 body: @creds,108 path: LOGIN_PATH)109 response = inspec.powershell(cmd)110 verify_pwsh_success!(response)111 else112 cmd = curl_command(request: 'POST',113 headers: nil,114 body: @creds,115 path: LOGIN_PATH)116 response = inspec.command(cmd)117 verify_curl_success!(response)118 end119 content = parse_response(response.stdout)120 verify_json_payload!(content)121 ENV['INSPEC_ARCHER_SESSION_TOKEN'] = content['RequestedObject']['SessionToken']122 end123 def securityparameter124 headers = { 'Authorization' => "Archer session-id=#{session_token}" }125 if inspec.os.windows?126 cmd = invoke_restmethod_command(request: 'GET',127 headers: headers,128 body: nil,129 path: SECURITY_PARAMETERS_PATH)130 response = inspec.powershell(cmd)131 verify_pwsh_success!(response)132 parse_response(response.stdout)['value']133 else134 cmd = curl_command(request: 'GET',135 headers: headers,136 body: nil,137 path: SECURITY_PARAMETERS_PATH)138 response = inspec.command(cmd)139 verify_curl_success!(response)140 parse_response(response.stdout)141 end142 end143 def verify_curl_success!(cmd)144 raise Inspec::Exceptions::ResourceSkipped, "Error fetching Archer data from curl #{@url}\nError: #{cmd.stderr.match(/curl: \(\d.\).*$/)}" unless cmd.exit_status.zero?145 end146 def verify_pwsh_success!(cmd)147 if cmd.stderr =~ /No HTTP resource was found that matches the request URI|DNS_FAIL/148 raise Inspec::Exceptions::ResourceSkipped, "Connection refused - please check the URL #{@url} for accuracy"149 end150 if cmd.stderr =~ /Could not establish trust relationship for the SSL\/TLS secure channel/151 raise Inspec::Exceptions::ResourceSkipped, 'Connection refused - peer certificate issuer is not recognized; try setting ssl_verify to false'152 end153 raise Inspec::Exceptions::ResourceSkipped, "Error fetching Archer data from Invoke-RestMethod #{@url}: #{cmd.stderr}" unless cmd.exit_status.zero?154 end155 def verify_json_payload!(content)156 raise Inspec::Exceptions::ResourceSkipped, "Message: #{content['ValidationMessages'][0]['MessageKey']}" unless content['IsSuccessful']#{url}\nError: #{cmd.stderr.match(/curl: \(\d.\).*$/)}""157 end158end...

Full Screen

Full Screen

run.rb

Source:run.rb Github

copy

Full Screen

...27 puts "Running inspec test #{@test} on #{server['host']}".colorize(:yellow)28 command = "bundle exec inspec exec test/#{@test} -i #{server['publickey']} -t ssh://#{server['user']}@#{server['host']}"29 end30 puts "command: #{command}".colorize(:light_blue)31 output, error, status = Open3.capture3(command, :chdir=>@cookbook_directory)32 if error33 error = "Server #{server['host']} not found. Do you have access to the server #{server['host']}?" if error.include?('SocketError')34 if error.include?('zlib(finalizer): the stream was freed prematurely.')35 # needed because of bug in zlib36 puts error.colorize(:green)37 puts output.colorize(:green)38 else39 raise error40 end41 else42 # no error43 puts output.colorize(:green)44 end45 unless status.success?46 raise "Process did not complete succesfully, please check output."47 end48 end49end50def validate51 raise "Stage not found. Did you specify the right stage on the command line? " if @stage.nil?52 raise "Project not found. Did you specify the right project on the command line? " if @project.nil?53 raise "Test spec not found. Did you specify the right test on the command line" if @test.nil?54 raise "Test spec not found. Does the file actually exist? " unless @test == 'all' || File.exists?("#{cookbook_directory}/test/#{@test}")55 raise "Cookbook directory not found. Did you configure the json config ~/.inspec-multi-server?" if @cookbook_directory.nil?56end57# error handler58on_error do |ex|59 # evaluate to true or false60 puts "Sorry, there was an issue! #{ex}".colorize(:red)61end...

Full Screen

Full Screen

error

Using AI Code Generation

copy

Full Screen

1describe file('/etc/passwd') do2 it { should exist }3describe file('/etc/passwd') do4 it { should exist }5describe file('/etc/passwd') do6 it { should exist }7describe file('/etc/passwd') do8 it { should exist }9describe file('/etc/passwd') do10 it { should exist }11describe file('/etc/passwd') do12 it { should exist }13describe file('/etc/passwd') do14 it { should exist }15describe file('/etc/passwd') do16 it { should exist }17describe file('/etc/passwd') do18 it { should exist }19describe file('/etc/passwd') do20 it { should exist }21describe file('/etc/passwd') do22 it { should exist }23describe file('/etc/passwd') do24 it { should exist }25describe file('/etc/passwd') do26 it { should exist }27describe file('/etc/passwd') do28 it { should exist }29describe file('/etc/passwd') do

Full Screen

Full Screen

error

Using AI Code Generation

copy

Full Screen

1 it { should cmp 'error' }2 it { should cmp 'error' }3 it { should cmp 'error' }4 it { should cmp 'error' }5 it { should cmp 'error' }6 it { should cmp 'error' }7 it { should cmp 'error' }8 it { should cmp 'error' }9 it { should cmp 'error' }10 it { should cmp 'error' }11 it { should cmp 'error' }

Full Screen

Full Screen

error

Using AI Code Generation

copy

Full Screen

1describe file('test.txt') do2 it { should exist }3describe file('test.txt') do4 it { should exist }5describe file('test.txt') do6 it { should exist }7describe file('test.txt') do8 it { should exist }9describe file('test.txt') do10 it { should exist }11describe file('test.txt') do12 it { should exist }

Full Screen

Full Screen

error

Using AI Code Generation

copy

Full Screen

1describe file('/tmp') do2 it { should be_directory }3describe file('/etc/hosts') do4 it { should be_file }5describe file('/etc/hosts') do6 it { should be_file }7 it { should be_owned_by 'root' }8 it { should be_readable }9describe file('/etc/hosts') do10 it { should be_file }11 it { should be_owned_by 'root' }12 it { should be_readable }13 its('content') { should match(/

Full Screen

Full Screen

error

Using AI Code Generation

copy

Full Screen

1inspec.error('This is an error message')2Inspec::Runner.new.error('This is an error message')3Inspec::Runner.new.error('This is an error message')4inspec.error('This is an error message')5Inspec::Runner.new.error('This is an error message')6Inspec::Runner.new.error('This is an error message')7inspec.error('This is an error message')8Inspec::Runner.new.error('This is an error message')9Inspec::Runner.new.error('This is an error message')10inspec.error('This is an error message')11Inspec::Runner.new.error('This is an error message')12Inspec::Runner.new.error('This is an error message')

Full Screen

Full Screen

error

Using AI Code Generation

copy

Full Screen

1describe file('/home/inspec/inspec-samples') do2 it { should exist }3describe file('/home/inspec/inspec-samples') do4 it { should exist }5describe file('/home/inspec/inspec-samples') do6 it { should exist }7describe file('/home/inspec/inspec-samples') do8 it { should exist }9describe file('/home/inspec/inspec-samples') do10 it { should exist }11describe file('/home/inspec/inspec-samples') do12 it { should exist }13describe file('/home/inspec/inspec-samples') do14 it { should exist }15describe file('/home/inspec/inspec-samples') do16 it { should exist }17describe file('/home/inspec/inspec-samples') do18 it { should exist }19describe file('/home/inspec/inspec-samples') do20 it { should exist }

Full Screen

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run Inspec_ruby automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Most used method in

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful