diff --git a/README.md b/README.md index 8bf3c23..9164a5b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Simple tasks to provision and tear_down containers / instances and virtual machi * [Docker](#docker) * [Vagrant](#vagrant) * [Vmpooler](#vmpooler) + * [Provision Service](#provision_service) 4. [Limitations - OS compatibility, etc.](#limitations) 5. [Development - Guide for contributing to the module](#development) @@ -214,6 +215,35 @@ Successful on 1 node: localhost Ran on 1 node in 4.52 seconds ``` +### Provision_service + +The provision service task is meant to be used from a Github Action workflow. + +Example usage: +Using the following provision.yaml file: + +``` +test_serv: + provisioner: provision::provision_service + params: + cloud: gcp + region: europe-west1 + zone: europe-west1-d + images: ['centos-7-v20200618', 'windows-server-2016-dc-v20200813'] +``` + +In the provision step you can invoke bundle exec rake 'litmus:provision_list[test_serv]' and this will ensure the creation of two VMs in GCP. + +Manual invokation of the provision service task from a workflow can be done using: +``` +bundle exec bolt --modulepath /Users/tp/workspace/git/ task run provision::provision_service --nodes localhost action=provision platform=centos-7-v20200813 inventory=/Users/tp/workspace/git/provision +``` +Or using Litmus: + +``` +bundle exec rake 'litmus:provision[provision::provision_service, centos-7-v20200813]' +``` + #### Synced Folders By default the task will provision a Vagrant box with the [synced folder]() **disabled**. diff --git a/tasks/provision_service.json b/tasks/provision_service.json new file mode 100644 index 0000000..3fcbd0e --- /dev/null +++ b/tasks/provision_service.json @@ -0,0 +1,31 @@ +{ + "puppet_task_version": 1, + "supports_noop": false, + "description": "Provision/Tear down a list of machines using the provisioning service", + "parameters": { + "action": { + "description": "Action to perform, tear_down or provision", + "type": "Enum[provision, tear_down]", + "default": "provision" + }, + "platform": { + "description": "Needed by litmus", + "type": "Optional[String[1]]" + }, + "node_name": { + "description": "Needed by litmus", + "type": "Optional[String[1]]" + }, + "inventory": { + "description": "Location of the inventory file", + "type": "Optional[String[1]]" + }, + "vars": { + "description": "The address of the provisioning service", + "type": "Optional[String[1]]" + } + }, + "files": [ + "provision/lib/task_helper.rb" + ] +} diff --git a/tasks/provision_service.rb b/tasks/provision_service.rb new file mode 100755 index 0000000..3c43bc5 --- /dev/null +++ b/tasks/provision_service.rb @@ -0,0 +1,135 @@ +#!/usr/bin/env ruby +require 'json' +require 'net/http' +require 'yaml' +require 'puppet_litmus' +require 'etc' +require_relative '../lib/task_helper' +include PuppetLitmus::InventoryManipulation + +def default_uri + 'https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision' +end + +def platform_to_cloud_request_parameters(platform, cloud, region, zone) + params = case platform + when String + { cloud: cloud, region: region, zone: zone, images: [platform] } + when Array + { cloud: cloud, region: region, zone: zone, images: platform } + else + platform[:cloud] = cloud unless cloud.nil? + platform[:images] = [platform[:images]] if platform[:images].is_a?(String) + platform + end + params +end + +# curl -X POST https://facade-validation-6f3kfepqcq-ew.a.run.app/v1/provision --data @test_machines.json +def invoke_cloud_request(params, uri, job_url, verb) + case verb.downcase + when 'post' + request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') + machines = [] + machines << params + request.body = { url: job_url, VMs: machines }.to_json + when 'delete' + request = Net::HTTP::Delete.new(uri) + request.body = { uuid: params }.to_json + else + raise StandardError "Unknown verb: '#{verb}'" + end + + File.open('request.json', 'wb') do |f| + f.write(request.body) + end + + req_options = { + use_ssl: uri.scheme == 'https', + read_timeout: 60 * 5, # timeout reads after 5 minutes - that's longer than the backend service would keep the request open + } + + response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http| + http.request(request) + end + # rubocop:disable Style/GuardClause + if response.code == '200' + return response.body + else + puts "ERROR CODE: #{response.code} - BODY: #{response.body}" + exit 1 + end + # rubocop:enable Style/GuardClause +end + +def provision(platform, inventory_location, vars) + # Call the provision service with the information necessary and write the inventory file locally + + job_url = ENV['GITHUB_URL'] || "https://api.github.com/repos/#{ENV['GITHUB_REPOSITORY']}/actions/runs/#{ENV['GITHUB_RUN_ID']}" + uri = URI.parse(ENV['SERVICE_URL'] || default_uri) + cloud = ENV['CLOUD'] + region = ENV['REGION'] + zone = ENV['ZONE'] + if job_url.nil? + data = JSON.parse(vars.tr(';', ',')) + job_url = data['job_url'] + end + inventory_full_path = File.join(inventory_location, 'inventory.yaml') + + params = platform_to_cloud_request_parameters(platform, cloud, region, zone) + response = invoke_cloud_request(params, uri, job_url, 'post') + if File.file?(inventory_full_path) + inventory_hash = inventory_hash_from_inventory_file(inventory_full_path) + response_hash = YAML.safe_load(response) + + inventory_hash['groups'].each do |g| + response_hash['groups'].each do |bg| + if g['name'] == bg['name'] + g['targets'] = g['targets'] + bg['targets'] + end + end + end + + File.open(inventory_full_path, 'w') { |f| f.write inventory_hash.to_yaml } + else + File.open('inventory.yaml', 'wb') do |f| + f.write(response) + end + end + { status: 'ok', node_name: platform } +end + +def tear_down(platform, inventory_location, _vars) + # remove all provisioned resources + uri = URI.parse(ENV['SERVICE_URL'] || default_uri) + + inventory_full_path = File.join(inventory_location, 'inventory.yaml') + # rubocop:disable Style/GuardClause + if File.file?(inventory_full_path) + inventory_hash = inventory_hash_from_inventory_file(inventory_full_path) + facts = facts_from_node(inventory_hash, platform) + job_id = facts['uuid'] + response = invoke_cloud_request(job_id, uri, '', 'delete') + return response.to_json + end + # rubocop:enable Style/GuardClause +end + +params = JSON.parse(STDIN.read) +platform = params['platform'] +action = params['action'] +vars = params['vars'] +node_name = params['node_name'] +inventory_location = sanitise_inventory_location(params['inventory']) +raise 'specify a node_name when tearing down' if action == 'tear_down' && node_name.nil? +raise 'specify a platform when provisioning' if action == 'provision' && platform.nil? + +begin + result = provision(platform, inventory_location, vars) if action == 'provision' + result = tear_down(node_name, inventory_location, vars) if action == 'tear_down' + puts result.to_json + exit 0 +rescue => e + puts({ _error: { kind: 'facter_task/failure', msg: e.message } }.to_json) + exit 1 +end