├── .gitignore ├── lib ├── beaker-lima │ └── version.rb └── beaker │ └── hypervisor │ ├── lima.rb │ └── lima_helper.rb ├── .editorconfig ├── .rubocop.yml ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── test.yml │ └── release.yml ├── Gemfile ├── acceptance └── config │ └── nodes │ └── hosts.yaml ├── .rubocop_todo.yml ├── README.md ├── beaker-lima.gemspec ├── spec ├── spec_helper.rb └── beaker │ └── hypervisor │ ├── lima_spec.rb │ └── lima_helper_spec.rb ├── Rakefile └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .vendor/ 3 | vendor/ 4 | bundle/ 5 | .bundle/ 6 | Gemfile.lock 7 | coverage/ 8 | -------------------------------------------------------------------------------- /lib/beaker-lima/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BeakerLima 4 | VERSION = '0.0.1' 5 | end 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | tab_width = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: 3 | - .rubocop_todo.yml 4 | 5 | inherit_gem: 6 | voxpupuli-rubocop: rubocop.yml 7 | 8 | Naming/FileName: 9 | Description: Some files violates the snake_case convention 10 | Exclude: 11 | - 'lib/beaker-lima.rb' -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # raise PRs for gem updates 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "13:00" 9 | open-pull-requests-limit: 10 10 | 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "13:00" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :coverage, optional: ENV['COVERAGE'] != 'yes' do 8 | gem 'codecov', require: false 9 | gem 'simplecov-console', require: false 10 | end 11 | 12 | group :release, optional: true do 13 | gem 'faraday-retry', '~> 2.1', require: false 14 | gem 'github_changelog_generator', '~> 1.16.4', require: false 15 | end 16 | -------------------------------------------------------------------------------- /acceptance/config/nodes/hosts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | HOSTS: 3 | ubuntu2204: 4 | platform: ubuntu-2204-aarch64 5 | roles: 6 | - agent 7 | hypervisor: lima 8 | lima: 9 | url: template://ubuntu-lts 10 | oraclelinux8: 11 | platform: el-8-aarch64 12 | roles: 13 | - master 14 | - agent 15 | - dashboard 16 | - database 17 | - classifier 18 | - default 19 | hypervisor: lima 20 | lima: 21 | url: template://oraclelinux-8 22 | CONFIG: 23 | nfs_server: none 24 | consoleport: 443 25 | log_level: verbose 26 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-04-24 16:47:48 UTC using RuboCop version 1.50.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: AllowSubject. 11 | RSpec/MultipleMemoizedHelpers: 12 | Max: 10 13 | 14 | RSpec/MultipleExpectations: 15 | Max: 10 16 | 17 | RSpec/ExampleLength: 18 | Max: 99 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beaker-lima 2 | 3 | [![License](https://img.shields.io/github/license/voxpupuli/beaker-lima.svg)](https://github.com/voxpupuli/beaker-lima/blob/master/LICENSE) 4 | [![Test](https://github.com/voxpupuli/beaker-lima/actions/workflows/test.yml/badge.svg)](https://github.com/voxpupuli/beaker-lima/actions/workflows/test.yml) 5 | [![Release](https://github.com/voxpupuli/beaker-lima/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/beaker-lima/actions/workflows/release.yml) 6 | [![RubyGem Version](https://img.shields.io/gem/v/beaker-lima.svg)](https://rubygems.org/gems/beaker-lima) 7 | [![RubyGem Downloads](https://img.shields.io/gem/dt/beaker-lima.svg)](https://rubygems.org/gems/beaker-lima) 8 | 9 | [Lima](https://github.com/lima-vm/lima) hypervisor for [Beaker](https://github.com/voxpupuli/beaker) acceptance testing framework 10 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 3 | 4 | changelog: 5 | exclude: 6 | labels: 7 | - duplicate 8 | - invalid 9 | - modulesync 10 | - question 11 | - skip-changelog 12 | - wont-fix 13 | - wontfix 14 | - github_actions 15 | 16 | categories: 17 | - title: Breaking Changes 🛠 18 | labels: 19 | - backwards-incompatible 20 | 21 | - title: New Features 🎉 22 | labels: 23 | - enhancement 24 | 25 | - title: Bug Fixes 🐛 26 | labels: 27 | - bug 28 | - bugfix 29 | 30 | - title: Documentation Updates 📚 31 | labels: 32 | - documentation 33 | - docs 34 | 35 | - title: Dependency Updates ⬆️ 36 | labels: 37 | - dependencies 38 | 39 | - title: Other Changes 40 | labels: 41 | - "*" 42 | -------------------------------------------------------------------------------- /beaker-lima.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 4 | require 'beaker-lima/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'beaker-lima' 8 | s.version = BeakerLima::VERSION 9 | s.summary = 'Lima hypervisor for Beaker acceptance testing framework' 10 | s.description = 'Allows running Beaker tests using Lima' 11 | s.authors = ['Yury Bushmelev', 'Vox Pupuli'] 12 | s.email = 'voxpupuli@groups.io' 13 | s.files = `git ls-files`.split("\n") 14 | s.homepage = 'https://github.com/voxpupuli/beaker-lima' 15 | s.license = 'Apache-2.0' 16 | 17 | s.required_ruby_version = '>= 2.7' 18 | 19 | s.add_development_dependency 'fakefs', '>= 1.3', '< 3.0' 20 | s.add_development_dependency 'rake' 21 | s.add_development_dependency 'rspec' 22 | s.add_development_dependency 'voxpupuli-rubocop', '~> 3.0' 23 | 24 | s.add_dependency 'bcrypt_pbkdf', '>= 1.0', '< 2.0' 25 | s.add_dependency 'beaker', '>= 5', '< 8' 26 | end 27 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'beaker' 4 | 5 | begin 6 | require 'simplecov' 7 | require 'simplecov-console' 8 | require 'codecov' 9 | rescue LoadError 10 | # Do nothing if no required gem installed 11 | else 12 | SimpleCov.start do 13 | track_files 'lib/**/*.rb' 14 | 15 | add_filter '/spec' 16 | # do not track vendored files 17 | add_filter '/vendor' 18 | add_filter '/.vendor' 19 | 20 | enable_coverage :branch 21 | end 22 | 23 | SimpleCov.formatters = [ 24 | SimpleCov::Formatter::Console, 25 | SimpleCov::Formatter::Codecov, 26 | ] 27 | end 28 | 29 | Dir['./lib/beaker/hypervisor/*.rb'].sort.each { |file| require file } 30 | 31 | # setup & require beaker's spec_helper.rb 32 | beaker_gem_spec = Gem::Specification.find_by_name('beaker') 33 | beaker_gem_dir = beaker_gem_spec.gem_dir 34 | beaker_spec_path = File.join(beaker_gem_dir, 'spec') 35 | $LOAD_PATH << beaker_spec_path 36 | require File.join(beaker_spec_path, 'spec_helper.rb') 37 | 38 | RSpec.configure do |config| 39 | config.include TestFileHelpers 40 | config.include HostHelpers 41 | end 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - master 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | rubocop: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Install Ruby 3.2 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: "3.2" 21 | bundler-cache: true 22 | - name: Run Rubocop 23 | run: bundle exec rake rubocop 24 | 25 | rspec: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - ruby: "2.7" 32 | - ruby: "3.0" 33 | coverage: "yes" 34 | - ruby: "3.1" 35 | - ruby: "3.2" 36 | env: 37 | COVERAGE: ${{ matrix.coverage }} 38 | name: RSpec - Ruby ${{ matrix.ruby }} 39 | steps: 40 | - uses: actions/checkout@v6 41 | - name: Install Ruby ${{ matrix.ruby }} 42 | uses: ruby/setup-ruby@v1 43 | with: 44 | ruby-version: ${{ matrix.ruby }} 45 | bundler-cache: true 46 | - name: spec tests 47 | run: bundle exec rake test:spec 48 | - name: Build gem 49 | run: gem build *.gemspec 50 | 51 | tests: 52 | needs: 53 | - rubocop 54 | - rspec 55 | runs-on: ubuntu-latest 56 | name: Test suite 57 | steps: 58 | - run: echo Test suite completed 59 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | begin 6 | require 'rubocop/rake_task' 7 | rescue LoadError 8 | # RuboCop is an optional group 9 | else 10 | RuboCop::RakeTask.new(:rubocop) do |task| 11 | # These make the rubocop experience maybe slightly less terrible 12 | task.options = ['--display-cop-names', '--display-style-guide', '--extra-details'] 13 | # Use Rubocop's Github Actions formatter if possible 14 | task.formatters << 'github' if ENV['GITHUB_ACTIONS'] == 'true' 15 | end 16 | end 17 | 18 | namespace :test do 19 | namespace :spec do 20 | desc 'Run spec tests' 21 | RSpec::Core::RakeTask.new(:run) do |t| 22 | t.rspec_opts = ['--color', '--format documentation'] 23 | t.pattern = 'spec/' 24 | end 25 | 26 | desc 'Run spec tests with coverage' 27 | RSpec::Core::RakeTask.new(:coverage) do |t| 28 | ENV['BEAKER_DOCKER_COVERAGE'] = 'y' 29 | t.rspec_opts = ['--color', '--format documentation'] 30 | t.pattern = 'spec/' 31 | end 32 | end 33 | 34 | namespace :acceptance do 35 | desc 'A quick acceptance test, named because it has no pre-suites to run' 36 | task :quick do 37 | # setup & load_path of beaker's acceptance base and lib directory 38 | beaker_gem_spec = Gem::Specification.find_by_name('beaker') 39 | beaker_gem_dir = beaker_gem_spec.gem_dir 40 | beaker_test_base_dir = File.join(beaker_gem_dir, 'acceptance/tests/base') 41 | load_path_option = File.join(beaker_gem_dir, 'acceptance/lib') 42 | keyfile = ENV['KEY'] || "#{Dir.home}/.ssh/id_rsa" 43 | 44 | beaker_cmd = [ 45 | 'beaker', 46 | '--hosts', 'acceptance/config/nodes/hosts.yaml', 47 | '--tests', beaker_test_base_dir, 48 | '--log-level', 'debug', 49 | '--load-path', load_path_option, 50 | ] 51 | beaker_cmd << '--keyfile' << keyfile if File.exist?(keyfile) 52 | sh(*beaker_cmd) 53 | end 54 | end 55 | end 56 | 57 | # namespace-named default tasks. 58 | # these are the default tasks invoked when only the namespace is referenced. 59 | # they're needed because `task :default` in those blocks doesn't work as expected. 60 | task 'test:spec': %i[test:spec:run] 61 | task 'test:acceptance': %i[test:acceptance:quick] 62 | 63 | # global defaults 64 | task test: %i[test:spec] 65 | task default: %i[test] 66 | 67 | begin 68 | require 'rubygems' 69 | require 'github_changelog_generator/task' 70 | rescue LoadError 71 | # Do nothing if no required gem installed 72 | else 73 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 74 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog] 75 | config.user = 'voxpupuli' 76 | config.project = 'beaker-lima' 77 | gem_version = Gem::Specification.load("#{config.project}.gemspec").version 78 | config.future_release = gem_version 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/lima.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Beaker 4 | # beaker extenstion to manage Lima VMs: https://github.com/lima-vm/lima 5 | class Lima < Beaker::Hypervisor 6 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act 7 | # upon, or a role (String or Symbol) that identifies one or more hosts. 8 | # @param [Hash{Symbol=>String}] options Options to pass on to the hypervisor 9 | def initialize(hosts, options) 10 | require 'beaker/hypervisor/lima_helper' 11 | 12 | super 13 | @logger = options[:logger] || Beaker::Logger.new 14 | @limahelper = options[:lima_helper] || LimaHelper.new(options) 15 | end 16 | 17 | def provision 18 | @logger.notify 'Provisioning Lima' 19 | @hosts.each do |host| 20 | @logger.notify "provisioning #{host.name}" 21 | @limahelper.start(host.name, host[:lima]) 22 | vm_opts = @limahelper.list([host.name]).first 23 | @logger.info "vm_opts: #{vm_opts}\n" 24 | setup_ssh(host) 25 | @logger.debug "node available at #{host[:ip]}:#{host[:port]}" 26 | end 27 | hack_etc_hosts @hosts, @options 28 | end 29 | 30 | def cleanup 31 | @logger.notify 'Cleaning up Lima' 32 | @hosts.each do |host| 33 | @logger.debug "stopping #{host.name}" 34 | @limahelper.stop(host.name) 35 | @limahelper.delete(host.name) 36 | end 37 | end 38 | 39 | def connection_preference(_host) 40 | [:ip] 41 | end 42 | 43 | private 44 | 45 | def setup_ssh(host) 46 | @logger.debug 'configure lima VMs (set ssh-config, switch to root user, hack etc/hosts)' 47 | 48 | default_user = host[:user] # root 49 | 50 | ssh_config = convert_ssh_opts(host) 51 | host[:ip] = '127.0.0.1' 52 | host[:port] = ssh_config[:port] 53 | host[:ssh] = host[:ssh].merge(ssh_config) 54 | host[:user] = ssh_config[:user] 55 | 56 | # copy user's keys to roots home dir, to allow for login as root 57 | copy_ssh_to_root host, @options 58 | # ensure that root login is enabled for this host 59 | enable_root_login host, @options 60 | # shut down connection, will reconnect on next exec 61 | host.close 62 | 63 | host[:user] = default_user 64 | host[:ssh][:user] = default_user 65 | end 66 | 67 | # Convert lima ssh opts to beaker (Net::SSH) ssh opts 68 | def convert_ssh_opts(host) 69 | cfg = @limahelper.ssh_info(host.name) 70 | forward_ssh_agent = @options[:forward_ssh_agent] || false 71 | keys_only = if @options[:forward_ssh_agent] == true 72 | false 73 | else 74 | (cfg['IdentitiesOnly'] || 'yes') == 'yes' 75 | end 76 | 77 | { 78 | forward_agent: forward_ssh_agent, 79 | host_name: cfg['Hostname'], 80 | keys: cfg['IdentityFile'], 81 | keys_only: keys_only, 82 | port: cfg['Port'], 83 | use_agent: forward_ssh_agent, 84 | user: cfg['User'], 85 | } 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Gem Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | build-release: 13 | # Prevent releases from forked repositories 14 | if: github.repository_owner == 'voxpupuli' 15 | name: Build the gem 16 | runs-on: ubuntu-24.04 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Install Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 'ruby' 23 | - name: Build gem 24 | shell: bash 25 | run: gem build --verbose *.gemspec 26 | - name: Upload gem to GitHub cache 27 | uses: actions/upload-artifact@v6 28 | with: 29 | name: gem-artifact 30 | path: '*.gem' 31 | retention-days: 1 32 | compression-level: 0 33 | 34 | create-github-release: 35 | needs: build-release 36 | name: Create GitHub release 37 | runs-on: ubuntu-24.04 38 | permissions: 39 | contents: write # clone repo and create release 40 | steps: 41 | - name: Download gem from GitHub cache 42 | uses: actions/download-artifact@v7 43 | with: 44 | name: gem-artifact 45 | - name: Create Release 46 | shell: bash 47 | env: 48 | GH_TOKEN: ${{ github.token }} 49 | run: gh release create --repo ${{ github.repository }} ${{ github.ref_name }} --generate-notes *.gem 50 | 51 | release-to-github: 52 | needs: build-release 53 | name: Release to GitHub 54 | runs-on: ubuntu-24.04 55 | permissions: 56 | packages: write # publish to rubygems.pkg.github.com 57 | steps: 58 | - name: Download gem from GitHub cache 59 | uses: actions/download-artifact@v7 60 | with: 61 | name: gem-artifact 62 | - name: Publish gem to GitHub packages 63 | run: gem push --host https://rubygems.pkg.github.com/${{ github.repository_owner }} *.gem 64 | env: 65 | GEM_HOST_API_KEY: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | release-to-rubygems: 68 | needs: build-release 69 | name: Release gem to rubygems.org 70 | runs-on: ubuntu-24.04 71 | environment: release # recommended by rubygems.org 72 | permissions: 73 | id-token: write # rubygems.org authentication 74 | steps: 75 | - name: Download gem from GitHub cache 76 | uses: actions/download-artifact@v7 77 | with: 78 | name: gem-artifact 79 | - uses: rubygems/configure-rubygems-credentials@v1.0.0 80 | - name: Publish gem to rubygems.org 81 | shell: bash 82 | run: gem push *.gem 83 | 84 | release-verification: 85 | name: Check that all releases are done 86 | runs-on: ubuntu-24.04 87 | permissions: 88 | contents: read # minimal permissions that we have to grant 89 | needs: 90 | - create-github-release 91 | - release-to-github 92 | - release-to-rubygems 93 | steps: 94 | - name: Download gem from GitHub cache 95 | uses: actions/download-artifact@v7 96 | with: 97 | name: gem-artifact 98 | - name: Install Ruby 99 | uses: ruby/setup-ruby@v1 100 | with: 101 | ruby-version: 'ruby' 102 | - name: Wait for release to propagate 103 | shell: bash 104 | run: | 105 | gem install rubygems-await 106 | gem await *.gem 107 | -------------------------------------------------------------------------------- /spec/beaker/hypervisor/lima_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | # Beaker::Lima unit tests 6 | module Beaker 7 | describe Lima do 8 | let(:hosts) do 9 | the_hosts = make_hosts 10 | the_hosts[0][:lima] = { url: 'template://ubuntu-lts' } 11 | the_hosts[1][:lima] = { config: { images: [] } } 12 | the_hosts 13 | end 14 | 15 | let(:logger) do 16 | logger = instance_double(Logger) 17 | allow(logger).to receive(:debug) 18 | allow(logger).to receive(:info) 19 | allow(logger).to receive(:warn) 20 | allow(logger).to receive(:error) 21 | allow(logger).to receive(:notify) 22 | logger 23 | end 24 | 25 | let(:options) do 26 | { 27 | logger: logger, 28 | lima_helper: lima_helper, 29 | forward_ssh_agent: true, 30 | provision: true, 31 | } 32 | end 33 | 34 | let(:ssh_info_hash) do 35 | { 36 | hosts[0].name => { 37 | 'IdentityFile' => [ 38 | '/home/test/.lima/_config/user', 39 | '/home/test/.ssh/id_rsa', 40 | ], 41 | 'PreferredAuthentications' => 'publickey', 42 | 'User' => 'test', 43 | 'Hostname' => '127.0.0.1', 44 | 'Port' => 54_321, 45 | }, 46 | hosts[1].name => { 47 | 'IdentityFile' => [ 48 | '/home/test/.lima/_config/user', 49 | '/home/test/.ssh/id_rsa', 50 | ], 51 | 'PreferredAuthentications' => 'publickey', 52 | 'User' => 'test', 53 | 'Hostname' => '127.0.0.1', 54 | 'Port' => 54_322, 55 | }, 56 | hosts[2].name => { 57 | 'IdentityFile' => [ 58 | '/home/test/.lima/_config/user', 59 | '/home/test/.ssh/id_rsa', 60 | ], 61 | 'PreferredAuthentications' => 'publickey', 62 | 'User' => 'test', 63 | 'Hostname' => '127.0.0.1', 64 | 'Port' => 54_323, 65 | }, 66 | } 67 | end 68 | 69 | let(:lima) { described_class.new(hosts, options) } 70 | 71 | let(:lima_helper) do 72 | lh = instance_double(LimaHelper) 73 | allow(lh).to receive(:info).and_return({ 'version' => '1.2.3' }) 74 | hosts.each do |host| 75 | # [list, status] are missing here because they depedns on a test case 76 | allow(lh).to receive(:start).with(host.name, host[:lima]).and_return(host.name) 77 | allow(lh).to receive(:stop).with(host.name).and_return(true) 78 | allow(lh).to receive(:ssh_info).with(host.name).and_return(ssh_info_hash[host.name]) 79 | allow(lh).to receive(:delete).with(host.name).and_return(true) 80 | end 81 | lh 82 | end 83 | 84 | describe '#provision' do 85 | it 'provisions the VMs' do 86 | hosts.each do |host| 87 | allow(lima_helper).to receive(:list).with([host.name]) 88 | .and_return([{ 'name' => host.name, 'status' => 'Running' }]) 89 | end 90 | 91 | lima.provision 92 | 93 | hosts.each do |host| 94 | expect(lima_helper).to have_received(:start).with(host.name, host[:lima]).once 95 | expect(lima_helper).to have_received(:ssh_info).with(host.name).once 96 | end 97 | end 98 | end 99 | 100 | describe '#cleanup' do 101 | it 'cleanups the VMs' do 102 | lima.cleanup 103 | 104 | hosts.each do |host| 105 | allow(lima_helper).to receive(:list).with([host.name]).and_return([]) 106 | 107 | expect(lima_helper).to have_received(:stop).with(host.name).once 108 | expect(lima_helper).to have_received(:delete).with(host.name).once 109 | end 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/lima_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Beaker 4 | # Beaker helper module to interact with Lima CLI 5 | class LimaHelper 6 | class LimaError < StandardError 7 | end 8 | 9 | def initialize(options) 10 | require 'json' 11 | require 'open3' 12 | require 'shellwords' 13 | 14 | @options = options 15 | @logger = options[:logger] 16 | 17 | @limactl = @options[:limactl] || 'limactl' 18 | @lima_info = nil 19 | @ssh_info = {} 20 | @timeout = @options[:timeout] || 600 # 10m 21 | end 22 | 23 | def info 24 | return @lima_info if @lima_info 25 | 26 | lima_cmd = [@limactl, 'info'] 27 | stdout_str, stderr_str, status = Open3.capture3(*lima_cmd) 28 | unless status.success? 29 | raise LimaError, "`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}" 30 | end 31 | 32 | @lima_info = JSON.parse(stdout_str) 33 | end 34 | 35 | def list(vm_names = []) 36 | lima_cmd = [@limactl, 'list', '--json'] 37 | vm_names.each { |vm_name| lima_cmd << vm_name } 38 | stdout_str, stderr_str, status = Open3.capture3(*lima_cmd) 39 | unless status.success? 40 | raise LimaError, "`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}" 41 | end 42 | 43 | stdout_str.split("\n").map { |vm| JSON.parse(vm) } 44 | end 45 | 46 | # A bit faster `list` variant to check the VM status 47 | def status(vm_name) 48 | lima_cmd = [@limactl, 'list', '--format', '{{ .Status }}', vm_name] 49 | stdout_str, stderr_str, status = Open3.capture3(*lima_cmd) 50 | unless status.success? 51 | raise LimaError, "`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}" 52 | end 53 | 54 | stdout_str.chomp 55 | end 56 | 57 | def start(vm_name, cfg = {}) 58 | case status(vm_name) 59 | when '' 60 | return create(vm_name, cfg) 61 | when 'Running' 62 | @logger.debug("'#{vm_name}' is running already, skipping...") 63 | return true 64 | end 65 | 66 | lima_cmd = [@limactl, 'start', "--timeout=#{@timeout}s", vm_name] 67 | _, stderr_str, status = Open3.capture3(*lima_cmd) 68 | unless status.success? 69 | raise LimaError, "`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}" 70 | end 71 | 72 | true 73 | end 74 | 75 | def create(vm_name, cfg = {}) 76 | @logger.debug("Options: #{cfg}") 77 | raise LimaError, 'Only one of url/template/config parameters must be specified' if cfg[:url] && cfg[:config] 78 | 79 | if cfg[:url] 80 | cfg_url = cfg[:url] 81 | elsif cfg[:config] 82 | # Write config to a temporary YAML file and pass it to limactl later 83 | safe_name = Shellwords.escape(vm_name) 84 | tmpfile = Tempfile.new(["lima_#{safe_name}", '.yaml']) 85 | # config has symbolized keys by default. So .to_yaml will write keys as :symbols. 86 | # Keys should be stringified to avoid this so Lima can parse the YAML properly. 87 | tmpfile.write(stringify_keys_recursively(cfg[:config]).to_yaml) 88 | tmpfile.close 89 | 90 | # Validate the config 91 | _, stderr_str, status = Open3.capture3(@limactl, 'validate', tmpfile.path) 92 | raise LimaError, "Config validation fails with error: #{stderr_str}" unless status.success? 93 | 94 | cfg_url = tmpfile.path 95 | else 96 | raise LimaError, 'At least one of url/template/config parameters must be specified' 97 | end 98 | 99 | lima_cmd = [@limactl, 'start', "--name=#{vm_name}", "--timeout=#{@timeout}s", cfg_url] 100 | _, stderr_str, status = Open3.capture3(*lima_cmd) 101 | tmpfile&.unlink # Delete tmpfile if any 102 | 103 | unless status.success? 104 | raise LimaError, "`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}" 105 | end 106 | 107 | true 108 | end 109 | 110 | def stop(vm_name) 111 | lima_cmd = [@limactl, 'stop', vm_name] 112 | _, stderr_str, status = Open3.capture3(*lima_cmd) 113 | 114 | # `limactl stop` might fail sometimes though VM is stopped actually 115 | # Performing additional check 116 | return true if status(vm_name) == 'Stopped' 117 | 118 | @logger.warn("`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}") 119 | false 120 | end 121 | 122 | def delete(vm_name) 123 | lima_cmd = [@limactl, 'delete', vm_name] 124 | _, stderr_str, status = Open3.capture3(*lima_cmd) 125 | 126 | # `limactl delete` might fail sometimes though VM is deleted actually 127 | # Performing additional check 128 | return true if status(vm_name).empty? 129 | 130 | @logger.warn("`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}") 131 | false 132 | end 133 | 134 | def ssh_info(vm_name) 135 | return @ssh_info[vm_name] if @ssh_info.key? vm_name 136 | 137 | lima_cmd = [@limactl, 'show-ssh', '--format', 'options', vm_name] 138 | stdout_str, stderr_str, status = Open3.capture3(*lima_cmd) 139 | 140 | if stdout_str.empty? 141 | @logger.warn("`#{lima_cmd.join(' ')}` failed with status #{status.exitstatus}: #{stderr_str}") 142 | return {} 143 | end 144 | 145 | # Convert key=value to [key],[value] pairs array 146 | vm_opts_pairs = Shellwords.shellwords(stdout_str).map { |x| x.split('=', 2) } 147 | 148 | # Collect all IdentityFile values 149 | identity_files = vm_opts_pairs.filter { |x| x[0] == 'IdentityFile' }.map { |x| x[1] } 150 | 151 | # Convert pairs array to a hash 152 | vm_opts = Hash[*vm_opts_pairs.flatten] 153 | vm_opts['IdentityFile'] = identity_files 154 | vm_opts['Port'] = vm_opts['Port'].to_i 155 | 156 | @ssh_info[vm_name] = vm_opts 157 | end 158 | 159 | # Stringify Hash keys recursively 160 | def stringify_keys_recursively(hash) 161 | stringified_hash = {} 162 | hash.each do |k, v| 163 | stringified_hash[k.to_s] = if v.is_a?(Hash) 164 | stringify_keys_recursively(v) 165 | elsif v.is_a?(Array) 166 | v.map { |x| x.is_a?(Hash) ? stringify_keys_recursively(x) : x } 167 | else 168 | v 169 | end 170 | end 171 | stringified_hash 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /spec/beaker/hypervisor/lima_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | # Beaker::LimaHelper unit tests 6 | module Beaker 7 | describe LimaHelper do 8 | let(:options) do 9 | { 10 | logger: logger, 11 | timeout: 900, 12 | } 13 | end 14 | let(:lima_success) do 15 | rc = instance_double(Process::Status) 16 | allow(rc).to receive_messages(success?: true, exitstatus: 0) 17 | rc 18 | end 19 | let(:lima_failure) do 20 | rc = instance_double(Process::Status) 21 | allow(rc).to receive_messages(success?: false, exitstatus: 123) 22 | rc 23 | end 24 | let(:lima_result) { lima_success } 25 | let(:vm_name) { 'test_vm' } 26 | let(:vm_status) { '' } 27 | let(:lima_helper) { described_class.new(options) } 28 | 29 | describe '#info' do 30 | let(:limactl_info) { { 'version' => '1.2.3' } } 31 | 32 | it 'returns `limactl info` output' do 33 | allow(Open3).to receive(:capture3).with('limactl', 'info') 34 | .and_return([limactl_info.to_json, '', lima_success]) 35 | 36 | result = lima_helper.info 37 | expect(Open3).to have_received(:capture3).with('limactl', 'info').once 38 | expect(result).to eq(limactl_info) 39 | end 40 | end 41 | 42 | describe '#list' do 43 | let(:limactl_list) do 44 | [ 45 | { 'name' => 'docker', 'status' => 'Running' }, 46 | { 'name' => 'podman', 'status' => 'Stopped' }, 47 | ] 48 | end 49 | let(:limactl_stdout) { limactl_list.map(&:to_json).join("\n") } 50 | 51 | it 'returns `limactl list` output' do 52 | allow(Open3).to receive(:capture3).with('limactl', 'list', '--json', 'docker', 'podman') 53 | .and_return([limactl_stdout, '', lima_success]) 54 | 55 | result = lima_helper.list(%w[docker podman]) 56 | expect(Open3).to have_received(:capture3).with('limactl', 'list', '--json', 'docker', 'podman').once 57 | expect(result).to eq(limactl_list) 58 | end 59 | end 60 | 61 | describe '#status' do 62 | let(:vm_status) { 'Running' } 63 | let(:limactl_list) { [{ 'name' => vm_name, 'status' => vm_status }] } 64 | 65 | it 'returns the VM status' do 66 | allow(Open3).to receive(:capture3).with('limactl', 'list', '--format', '{{ .Status }}', vm_name) 67 | .and_return([vm_status, '', lima_result]) 68 | 69 | result = lima_helper.status(vm_name) 70 | expect(Open3).to have_received(:capture3).with('limactl', 'list', '--format', '{{ .Status }}', vm_name).once 71 | expect(result).to eq(vm_status) 72 | end 73 | end 74 | 75 | describe '#start' do 76 | context 'with existsing VM' do 77 | let(:vm_status) { 'Stopped' } 78 | 79 | it 'starts the VM' do 80 | allow(lima_helper).to receive(:status).and_return(vm_status) 81 | allow(Open3).to receive(:capture3).with('limactl', 'start', "--timeout=#{options[:timeout]}s", vm_name) 82 | .and_return([vm_status, '', lima_success]) 83 | 84 | result = lima_helper.start(vm_name) 85 | expect(lima_helper).to have_received(:status).once 86 | expect(Open3).to have_received(:capture3) 87 | .with('limactl', 'start', "--timeout=#{options[:timeout]}s", vm_name) 88 | .once 89 | expect(result).to be true 90 | end 91 | end 92 | 93 | context 'with non-existent VM name' do 94 | it 'creates the VM' do 95 | allow(lima_helper).to receive(:status).and_return(vm_status) 96 | allow(lima_helper).to receive(:create).with(vm_name, {}).and_return(true) 97 | 98 | result = lima_helper.start(vm_name) 99 | expect(lima_helper).to have_received(:status).once 100 | expect(lima_helper).to have_received(:create).with(vm_name, {}).once 101 | expect(result).to be true 102 | end 103 | end 104 | end 105 | 106 | describe '#create' do 107 | context 'with url' do 108 | let(:cfg) { { url: 'template://ubuntu-lts' } } 109 | 110 | it 'creates the VM' do 111 | allow(Open3).to receive(:capture3) 112 | .with('limactl', 'start', "--name=#{vm_name}", "--timeout=#{options[:timeout]}s", cfg[:url]) 113 | .and_return(['', '', lima_success]) 114 | 115 | result = lima_helper.create(vm_name, cfg) 116 | expect(Open3).to have_received(:capture3) 117 | .with('limactl', 'start', "--name=#{vm_name}", "--timeout=#{options[:timeout]}s", cfg[:url]) 118 | .once 119 | expect(result).to be true 120 | end 121 | end 122 | 123 | context 'with config' do 124 | let(:cfg) { { config: { images: ['https://example.com/lima.qcow2'] } } } 125 | let(:tmpfile) { Tempfile.new(["lima_#{vm_name}", '.yaml']) } 126 | 127 | before do 128 | allow(Tempfile).to receive(:new).and_return(tmpfile) 129 | end 130 | 131 | after do 132 | tmpfile.close 133 | tmpfile.unlink 134 | end 135 | 136 | it 'creates the VM' do 137 | saved_path = tmpfile.path 138 | allow(Open3).to receive(:capture3).with('limactl', 'validate', saved_path) 139 | .and_return(['', '', lima_success]) 140 | allow(Open3).to receive(:capture3) 141 | .with('limactl', 'start', "--name=#{vm_name}", "--timeout=#{options[:timeout]}s", saved_path) 142 | .and_return(['', '', lima_success]) 143 | 144 | result = lima_helper.create(vm_name, cfg) 145 | expect(Open3).to have_received(:capture3).with('limactl', 'validate', saved_path).once 146 | expect(Open3).to have_received(:capture3) 147 | .with('limactl', 'start', "--name=#{vm_name}", "--timeout=#{options[:timeout]}s", saved_path) 148 | .once 149 | expect(result).to be true 150 | end 151 | end 152 | end 153 | 154 | describe '#stop' do 155 | let(:vm_status) { 'Stopped' } 156 | 157 | it 'stops the VM' do 158 | allow(lima_helper).to receive(:status).and_return(vm_status) 159 | allow(Open3).to receive(:capture3).with('limactl', 'stop', vm_name) 160 | .and_return(['', '', lima_success]) 161 | 162 | result = lima_helper.stop(vm_name) 163 | expect(lima_helper).to have_received(:status).once 164 | expect(Open3).to have_received(:capture3).with('limactl', 'stop', vm_name).once 165 | expect(result).to be(true) 166 | end 167 | end 168 | 169 | describe '#delete' do 170 | it 'deletes the VM' do 171 | allow(lima_helper).to receive(:status).and_return(vm_status) 172 | allow(Open3).to receive(:capture3).with('limactl', 'delete', vm_name) 173 | .and_return(['', '', lima_success]) 174 | 175 | result = lima_helper.delete(vm_name) 176 | expect(lima_helper).to have_received(:status).once 177 | expect(Open3).to have_received(:capture3).with('limactl', 'delete', vm_name).once 178 | expect(result).to be(true) 179 | end 180 | end 181 | 182 | describe '#ssh_info' do 183 | let(:limactl_stdout) do 184 | <<~LIMA 185 | IdentityFile="/home/test/.lima/_config/user" 186 | IdentityFile="/home/test/.ssh/id_rsa" 187 | PreferredAuthentications=publickey 188 | User=test 189 | Hostname=127.0.0.1 190 | Port=54321 191 | LIMA 192 | end 193 | let(:ssh_info) do 194 | { 195 | 'IdentityFile' => [ 196 | '/home/test/.lima/_config/user', 197 | '/home/test/.ssh/id_rsa', 198 | ], 199 | 'PreferredAuthentications' => 'publickey', 200 | 'User' => 'test', 201 | 'Hostname' => '127.0.0.1', 202 | 'Port' => 54_321, 203 | } 204 | end 205 | 206 | it 'returns the VM ssh connection info' do 207 | allow(Open3).to receive(:capture3).with('limactl', 'show-ssh', '--format', 'options', vm_name) 208 | .and_return([limactl_stdout, '', lima_success]) 209 | 210 | result = lima_helper.ssh_info(vm_name) 211 | expect(Open3).to have_received(:capture3).with('limactl', 'show-ssh', '--format', 'options', vm_name).once 212 | expect(result).to eq(ssh_info) 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------