Skip to content

Commit

Permalink
Fixes #36971 - GUI to allow cloning of Ansible roles from VCS
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorben-D committed May 3, 2024
1 parent 8c6be4f commit be5b27a
Show file tree
Hide file tree
Showing 26 changed files with 1,667 additions and 4 deletions.
150 changes: 150 additions & 0 deletions app/controllers/api/v2/vcs_clone_controller.rb
@@ -0,0 +1,150 @@
module Api
module V2
class VcsCloneController < ::Api::V2::BaseController
include ::ForemanAnsible::ProxyAPI
include ::Api::Version2

resource_description do
api_version 'v2'
api_base_url '/ansible/api'
end

def_param_group :repo_information do
param :repo_info, Hash, :desc => N_('Hash containing info about the Ansible role to be installed') do
param :vcs_url, String, :desc => N_('URL of the repository'), :required => true
param :role_name, String, :desc => N_('Name of the Ansible role'), :required => true
param :ref, String, :desc => N_('Branch / Tag / Commit reference'), :required => true
end
end

rescue_from ActionController::ParameterMissing do |e|
render json: { 'error' => e.message }, status: :bad_request
end

skip_before_action :verify_authenticity_token

before_action :set_proxy_api

api :GET, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/repository_metadata',
N_('Retrieve metadata about the repository associated with a Smart Proxy.')
param :smart_proxy_id, Array, N_('Name of the Smart Proxy'), :required => true
param :vcs_url, String, N_('URL of the repository'), :required => true
error 400, :desc => N_('Invalid or missing parameters')
def repository_metadata
vcs_url = params.require(:vcs_url)
render json: @proxy_api.repo_information(vcs_url)
end

api :GET, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles',
N_('Returns an array of Ansible roles installed on the provided Smart Proxy')
formats ['json']
param :smart_proxy_id, Array, N_('Name of the SmartProxy'), :required => true
error 400, :desc => N_('Invalid or missing parameters')
def installed_roles
render json: @proxy_api.list_installed
end

api :POST, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles',
N_('Launches a task to install the provided role')
formats ['json']
param_group :repo_information
param :smart_proxy_id, Array, N_('Smart Proxy where the role should be installed')
error 400, :desc => N_('Invalid or missing parameters')
def install_role
payload = verify_install_role_parameters(params)
start_vcs_task(payload, :install)
end

api :PUT, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles',
N_('Launches a task to update the provided role')
formats ['json']
param_group :repo_information
param :smart_proxy_id, Array, N_('Smart Proxy where the role should be installed')
error 400, :desc => N_('Invalid or missing parameters')
def update_role
payload = verify_update_role_parameters(params)
payload['name'] = params.require(:role_name)
start_vcs_task(payload, :update)
end

api :DELETE, '/smart_proxies/:smart_proxy_id/ansible/vcs_clone/roles/:role_name',
N_('Launches a task to delete the provided role')
formats ['json']
param :role_name, String, :desc => N_('Name of the role to be deleted')
param :smart_proxy_id, Array, N_('Smart Proxy to delete the role from')
error 400, :desc => N_('Invalid or missing parameters')
def delete_role
payload = params.require(:role_name)
start_vcs_task(payload, :delete)
end

private

def set_proxy_api
unless params[:id]
msg = _('Smart proxy id is required')
return render_error('custom_error', :status => :unprocessable_entity, :locals => { :message => msg })
end
ansible_proxy = SmartProxy.find_by(id: params[:id])
if ansible_proxy.nil?
msg = _('Smart proxy does not exist')
return render_error('custom_error', :status => :bad_request, :locals => { :message => msg })
else unless ansible_proxy.has_capability?('Ansible', 'vcs_clone')
msg = _('Smart Proxy is missing foreman_ansible installation or Git cloning capability')
return render_error('custom_error', :status => :bad_request, :locals => { :message => msg })
end
end
@proxy = ansible_proxy
@proxy_api = find_proxy_api(ansible_proxy)
end

def permit_parameters(params)
params.require(:vcs_clone).
permit(
repo_info: [
:vcs_url,
:role_name,
:ref
]
).to_h
end

def verify_install_role_parameters(params)
payload = permit_parameters params
%w[vcs_url role_name ref].each do |param|
raise ActionController::ParameterMissing.new(param) unless payload['repo_info'].key?(param)
end
payload
end

def verify_update_role_parameters(params)
payload = permit_parameters params
%w[vcs_url ref].each do |param|
raise ActionController::ParameterMissing.new(param) unless payload['repo_info'].key?(param)
end
payload
end

def start_vcs_task(op_info, operation)
case operation
when :update
job = UpdateAnsibleRole.perform_later(op_info, @proxy)
when :install
job = CloneAnsibleRole.perform_later(op_info, @proxy)
when :delete
job = DeleteAnsibleRole.perform_later(op_info, @proxy)
else
raise Foreman::Exception.new(N_('Unsupported operation'))
end

task = ForemanTasks::Task.find_by(external_id: job.provider_job_id)

render json: {
task: task
}, status: :ok
rescue Foreman::Exception
head :internal_server_error
end
end
end
end
6 changes: 6 additions & 0 deletions app/helpers/foreman_ansible/ansible_roles_helper.rb
Expand Up @@ -31,6 +31,12 @@ def ansible_proxy_import(hash)
ansible_proxy_links(hash))
end

def vcs_import
select_action_button("",
{ :primary => true, :class => 'roles-import' },
link_to(_("Download from Git"), "#vcs_download"))
end

def import_time(role)
_('%s ago') % time_ago_in_words(role.updated_at)
end
Expand Down
12 changes: 12 additions & 0 deletions app/jobs/clone_ansible_role.rb
@@ -0,0 +1,12 @@
class CloneAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Download Ansible Role from Git')
end

def perform(repo_info, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.install_role repo_info
end
end
12 changes: 12 additions & 0 deletions app/jobs/delete_ansible_role.rb
@@ -0,0 +1,12 @@
class DeleteAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Delete Ansible Role from Smart Proxy')
end

def perform(role_name, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.delete_role role_name
end
end
12 changes: 12 additions & 0 deletions app/jobs/update_ansible_role.rb
@@ -0,0 +1,12 @@
class UpdateAnsibleRole < ::ApplicationJob
queue_as :default

def humanized_name
_('Update Ansible Role from Git')
end

def perform(repo_info, proxy)
vcs_cloner = ForemanAnsible::VcsCloner.new(proxy)
vcs_cloner.update_role repo_info
end
end
59 changes: 58 additions & 1 deletion app/lib/proxy_api/ansible.rb
Expand Up @@ -4,7 +4,7 @@ module ProxyAPI
# ProxyAPI for Ansible
class Ansible < ::ProxyAPI::Resource
def initialize(args)
@url = args[:url] + '/ansible/'
@url = "#{args[:url]}/ansible/"
super args
end

Expand Down Expand Up @@ -53,5 +53,62 @@ def playbooks(playbooks_names = [])
rescue *PROXY_ERRORS => e
raise ProxyException.new(url, e, N_('Unable to get playbooks from Ansible'))
end

def repo_information(vcs_url)
parse(get("vcs_clone/repo_information?vcs_url=#{vcs_url}"))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 400
raise Foreman::Exception.new N_('Error requesting repository metadata. Check Smart Proxy log.')
else
raise
end
end

def list_installed
parse(get('vcs_clone/roles'))
rescue *PROXY_ERRORS
raise Foreman::Exception.new N_('Error requesting installed roles. Check log.')
end

def install_role(repo_info)
parse(post(repo_info, 'vcs_clone/roles'))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 409
raise Foreman::Exception.new N_('A repo with the name %s already exists.') % repo_info['repo_info']&.[]('name')
when 400
raise Foreman::Exception.new N_('Git Error. Check log.')
else
raise
end
end

def update_role(repo_info)
name = repo_info.delete('name')
parse(put(repo_info, "vcs_clone/roles/#{name}"))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 400
raise Foreman::Exception.new N_('Error updating %s. Check Smartproxy log.') % name
else
raise
end
end

def delete_role(role_name)
parse(delete("vcs_clone/roles/#{role_name}"))
rescue *PROXY_ERRORS, RestClient::Exception => e
raise e unless e.is_a? RestClient::RequestFailed
case e.http_code
when 400
raise Foreman::Exception.new N_('Error deleting %s. Check Smartproxy log.') % role_name
else
raise
end
end
end
end
15 changes: 15 additions & 0 deletions app/services/foreman_ansible/vcs_cloner.rb
@@ -0,0 +1,15 @@
module ForemanAnsible
class VcsCloner
include ::ForemanAnsible::ProxyAPI

def initialize(proxy = nil)
@ansible_proxy = proxy
end

delegate :install_role, to: :proxy_api

delegate :update_role, to: :proxy_api

delegate :delete_role, to: :proxy_api
end
end
12 changes: 10 additions & 2 deletions app/views/ansible_roles/index.html.erb
@@ -1,7 +1,13 @@
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= csrf_meta_tag %>
<% title _("Ansible Roles") %>
<% title_actions ansible_proxy_import(hash_for_import_ansible_roles_path),
documentation_button('#4.1ImportingRoles', :root_url => ansible_doc_url) %>
<% title_actions ansible_proxy_import(hash_for_import_ansible_roles_path), vcs_import,
documentation_button('#4.1ImportingRoles', :root_url => ansible_doc_url)
%>

<table class="<%= table_css_classes 'table-fixed' %>">
<thead>
Expand Down Expand Up @@ -44,4 +50,6 @@
</tbody>
</table>

<%= react_component('VcsCloneModalContent')%>
<%= will_paginate_with_info @ansible_roles %>
6 changes: 6 additions & 0 deletions app/views/ansible_roles/welcome.html.erb
@@ -1,3 +1,6 @@
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= webpacked_plugins_css_for :foreman_ansible %>
<% content_for(:title, _("Ansible Roles")) %>
<div class="blank-slate-pf">
<div class="blank-slate-pf-icon">
Expand All @@ -10,5 +13,8 @@
<p><%= link_to(_('Learn more about this in the documentation.'), documentation_url('#4.1ImportingRoles', :root_url => ansible_doc_url), target: '_blank') %></p>
<div class="blank-slate-pf-secondary-action">
<%= ansible_proxy_import(hash_for_import_ansible_roles_path) %>
<%= vcs_import %>
</div>
</div>

<%= react_component('VcsCloneModalContent', {:title => "Get Ansible-Roles from VCS"})%>
9 changes: 9 additions & 0 deletions config/routes.rb
Expand Up @@ -32,6 +32,15 @@
post :multiple_play_roles
end
end
resources :smart_proxies, :only => [] do
member do
get 'repository_metadata', to: 'vcs_clone#repository_metadata'
get 'roles', to: 'vcs_clone#installed_roles'
post 'roles', to: 'vcs_clone#install_role'
put 'roles/:role_name', to: 'vcs_clone#update_role', constraints: { role_name: %r{[^\/]+} }
delete 'roles/:role_name', to: 'vcs_clone#delete_role', constraints: { role_name: %r{[^\/]+} }
end
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/foreman_ansible/register.rb
Expand Up @@ -160,6 +160,8 @@
{ :'api/v2/ansible_inventories' => [:schedule] }
permission :import_ansible_playbooks,
{ :'api/v2/ansible_playbooks' => [:sync, :fetch] }
permission :clone_from_vcs,
{ :'api/v2/vcs_clone' => [:repo_information, :installed_roles, :install_role, :update_role, :delete_role] }
end

role 'Ansible Roles Manager',
Expand All @@ -170,7 +172,7 @@
:import_ansible_roles, :view_ansible_variables, :view_lookup_values,
:create_lookup_values, :edit_lookup_values, :destroy_lookup_values,
:create_ansible_variables, :import_ansible_variables,
:edit_ansible_variables, :destroy_ansible_variables, :import_ansible_playbooks]
:edit_ansible_variables, :destroy_ansible_variables, :import_ansible_playbooks, :clone_from_vcs]

role 'Ansible Tower Inventory Reader',
[:view_hosts, :view_hostgroups, :view_facts, :generate_report_templates, :generate_ansible_inventory,
Expand Down

0 comments on commit be5b27a

Please sign in to comment.