Project

General

Profile

Download (30 KB) Statistics
| Branch: | Tag: | Revision:
#
# Copyright 2014 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public
# License as published by the Free Software Foundation; either version
# 2 of the License (GPLv2) or (at your option) any later version.
# There is NO WARRANTY for this software, express or implied,
# including the implied warranties of MERCHANTABILITY,
# NON-INFRINGEMENT, or FITNESS FOR A PARTICULAR PURPOSE. You should
# have received a copy of GPLv2 along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.

# rubocop:disable SymbolName
module Katello
module Resources

require 'rest_client'

module Candlepin

class Proxy
def self.logger
::Logging.logger['cp_proxy']
end

def self.post(path, body)
logger.debug "Sending POST request to Candlepin: #{path}"
client = CandlepinResource.rest_client(Net::HTTP::Post, :post, path_with_cp_prefix(path))
client.post body, {:accept => :json, :content_type => :json}.merge(User.cp_oauth_header)
end

def self.delete(path, body = nil)
logger.debug "Sending DELETE request to Candlepin: #{path}"
client = CandlepinResource.rest_client(Net::HTTP::Delete, :delete, path_with_cp_prefix(path))
# Some candlepin calls will set the body in DELETE requests.
client.options[:payload] = body unless body.nil?
client.delete({:accept => :json, :content_type => :json}.merge(User.cp_oauth_header))
end

def self.get(path)
logger.debug "Sending GET request to Candlepin: #{path}"
client = CandlepinResource.rest_client(Net::HTTP::Get, :get, path_with_cp_prefix(path))
client.get({:accept => :json}.merge(User.cp_oauth_header))
end

def self.put(path, body)
logger.debug "Sending PUT request to Candlepin: #{path}"
client = CandlepinResource.rest_client(Net::HTTP::Put, :put, path_with_cp_prefix(path))
client.put body, {:accept => :json, :content_type => :json}.merge(User.cp_oauth_header)
end

def self.path_with_cp_prefix(path)
CandlepinResource.prefix + path
end

end

class CandlepinResource < HttpResource
cfg = Katello.config.candlepin
url = cfg.url
self.prefix = URI.parse(url).path
self.site = url.gsub(self.prefix, "")
self.consumer_secret = cfg.oauth_secret
self.consumer_key = cfg.oauth_key
self.ca_cert_file = cfg.ca_cert_file

def self.logger
::Logging.logger['cp_rest']
end

def self.default_headers(uuid = nil)
# There are cases where virt-who needs to act on behalf of hypervisors it is managing.
# If the uuid is specified, then that consumer is used in the headers rather than the
# virt-who consumer uuid.
# Current example is creating a hypervisor that in turn needs to get compliance.
if !uuid.nil? && User.consumer?
cp_oauth_header = { 'cp-consumer' => uuid }
else
cp_oauth_header = User.cp_oauth_header
end

{'accept' => 'application/json',
'accept-language' => I18n.locale,
'content-type' => 'application/json'}.merge(cp_oauth_header)
end

def self.name_to_key(a_name)
a_name.tr(' ', '_')
end
end

class CandlepinPing < CandlepinResource
class << self
def ping
response = get('/candlepin/status').body
JSON.parse(response).with_indifferent_access
end

def distributor_versions
response = get("/candlepin/distributor_versions").body
JSON.parse(response)
end
end
end

class Consumer < CandlepinResource
class << self
def path(id = nil)
"/candlepin/consumers/#{id}"
end

def get(params)
if params.is_a?(String)
JSON.parse(super(path(params), self.default_headers).body).with_indifferent_access
else
response = super(path + hash_to_query(params), self.default_headers).body
JSON.parse(response)
end
end

# rubocop:disable ParameterLists
def create(env_id, key, name, type, facts, installed_products, autoheal = true, release_ver = nil,
service_level = "", uuid = "", capabilities = nil, activation_keys = [], guest_ids = [])
# rubocop:enable ParameterLists

activation_key_ids = activation_keys.collect do |activation_key|
activation_key.cp_name
end

url = "/candlepin/environments/#{url_encode(env_id)}/consumers/"
attrs = {:name => name,
:type => type,
:facts => facts,
:installedProducts => installed_products,
:autoheal => autoheal,
:releaseVer => release_ver,
:serviceLevel => service_level,
:uuid => uuid,
:capabilities => capabilities,
:guestIds => guest_ids }
url += "?activation_keys=" + activation_key_ids.join(",") if activation_key_ids.length > 0
response = self.post(url, attrs.to_json, self.default_headers).body
JSON.parse(response).with_indifferent_access
end

def register_hypervisors(params)
url = "/candlepin/hypervisors"
url << "?owner=#{params[:owner]}&env=#{params[:env]}"
attrs = params.except(:owner, :env)
response = self.post(url, attrs.to_json, self.default_headers).body
JSON.parse(response).with_indifferent_access
end

def update(uuid, facts, guest_ids = nil, installed_products = nil, autoheal = nil, release_ver = nil,
service_level = nil, environment_id = nil, capabilities = nil, last_checkin = nil)
attrs = {:facts => facts,
:guestIds => guest_ids,
:releaseVer => release_ver,
:installedProducts => installed_products,
:autoheal => autoheal,
:serviceLevel => service_level,
:environment => environment_id.nil? ? nil : {:id => environment_id},
:capabilities => capabilities,
:lastCheckin => last_checkin
}.delete_if { |k, v| v.nil? }
if attrs.empty?
return true
else
self.put(path(uuid), attrs.to_json, self.default_headers).body
end
# consumer update doesn't return any data atm
# JSON.parse(response).with_indifferent_access
end

def destroy(uuid)
self.delete(path(uuid), User.cp_oauth_header).code.to_i
end

def checkin(uuid, checkin_date)
checkin_date ||= DateTime.now
self.put(path(uuid), {:lastCheckin => checkin_date}.to_json, self.default_headers).body
end

def available_pools(uuid, listall = false)
url = Pool.path + "?consumer=#{uuid}&listall=#{listall}"
response = Candlepin::CandlepinResource.get(url, self.default_headers).body
JSON.parse(response)
end

def regenerate_identity_certificates(uuid)
response = self.post(path(uuid), {}, self.default_headers).body
JSON.parse(response).with_indifferent_access
end

def export(uuid)
# Export is a zip file
headers = self.default_headers
headers['accept'] = 'application/zip'
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'export'), headers)
response
end

def entitlements(uuid)
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'entitlements'), self.default_headers).body
Util::Data.array_with_indifferent_access JSON.parse(response)
end

def refresh_entitlements(uuid)
self.post(join_path(path(uuid), 'entitlements'), "", self.default_headers).body
end

def consume_entitlement(uuid, pool, quantity = nil)
uri = join_path(path(uuid), 'entitlements') + "?pool=#{pool}"
uri += "&quantity=#{quantity}" if quantity
response = self.post(uri, "", self.default_headers).body
response.blank? ? [] : JSON.parse(response)
end

def remove_entitlement(uuid, ent_id)
uri = join_path(path(uuid), 'entitlements') + "/#{ent_id}"
self.delete(uri, self.default_headers).code.to_i
end

def remove_entitlements(uuid)
uri = join_path(path(uuid), 'entitlements')
self.delete(uri, self.default_headers).code.to_i
end

def remove_certificate(uuid, serial_id)
uri = join_path(path(uuid), 'certificates') + "/#{serial_id}"
self.delete(uri, self.default_headers).code.to_i
end

def virtual_guests(uuid)
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'guests'), self.default_headers).body
Util::Data.array_with_indifferent_access JSON.parse(response)
rescue
return []
end

def virtual_host(uuid)
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'host'), self.default_headers).body
if response.present?
JSON.parse(response).with_indifferent_access
else
return nil
end
rescue
return nil
end

def compliance(uuid)
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'compliance'), self.default_headers(uuid)).body
if response.present?
json = JSON.parse(response).with_indifferent_access
if json['reasons']
json['reasons'].sort!{|x, y| x['attributes']['name'] <=> y['attributes']['name']}
else
json['reasons'] = []
end
json
else
return nil
end
end

def events(uuid)
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'events'), self.default_headers).body
if response.present?
Util::Data.array_with_indifferent_access JSON.parse(response)
else
return []
end
end

def content_overrides(id)
result = Candlepin::CandlepinResource.get(join_path(path(id), 'content_overrides'), self.default_headers).body
Util::Data.array_with_indifferent_access(JSON.parse(result))
end

def update_content_override(id, content_label, name, value = nil)
attrs = [{ :contentLabel => content_label, :name => name }]
if value
attrs[0][:value] = value
result = Candlepin::CandlepinResource.put(join_path(path(id), 'content_overrides'),
attrs.to_json, self.default_headers)
else
client = Candlepin::CandlepinResource.rest_client(Net::HTTP::Delete, :delete,
join_path(path(id), 'content_overrides'))
client.options[:payload] = attrs.to_json
result = client.delete({:accept => :json, :content_type => :json}.merge(User.cp_oauth_header))
end
Util::Data.array_with_indifferent_access(JSON.parse(result))
end

end
end

class UpstreamConsumer < HttpResource

def self.logger
::Logging.logger['cp_rest']
end

def self.resource(url, client_cert, client_key, ca_file)
RestClient::Resource.new(url,
:ssl_client_cert => OpenSSL::X509::Certificate.new(client_cert),
:ssl_client_key => OpenSSL::PKey::RSA.new(client_key),
:ssl_ca_file => ca_file,
:verify_ssl => ca_file ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
)
end

def self.export(url, client_cert, client_key, ca_file)

logger.debug "Sending GET request to upstream Candlepin: #{url}"
return resource(url, client_cert, client_key, ca_file).get
rescue => e
raise e
end

def self.update(url, client_cert, client_key, ca_file, attributes)

logger.debug "Sending POST request to upstream Candlepin: #{url} #{attributes.to_json}"

return resource(url, client_cert, client_key, ca_file).put(attributes.to_json,
{'accept' => 'application/json',
'accept-language' => I18n.locale,
'content-type' => 'application/json'})
end

end

class OwnerInfo < CandlepinResource
class << self

def find(key)
owner_json = self.get(path(key), {'accept' => 'application/json'}.merge(User.cp_oauth_header)).body
JSON.parse(owner_json).with_indifferent_access
end

def path(id = nil)
"/candlepin/owners/#{id}/info"
end
end
end

class Owner < CandlepinResource
class << self
# Set the contentPrefix at creation time so that the client will get
# content only for the org it has been subscribed to
def create(key, description)
attrs = {:key => key, :displayName => description, :contentPrefix => (Katello.config.katello? ? "/#{key}/$env" : "")}
owner_json = self.post(path, attrs.to_json, self.default_headers).body
JSON.parse(owner_json).with_indifferent_access
end

# create the first user for owner
def create_user(key, username, password)
# create user with superadmin flag (no role, permissions etc)
CPUser.create(:username => name_to_key(username), :password => name_to_key(password), :superAdmin => true)
end

def destroy(key)
self.delete(path(key), User.cp_oauth_header).code.to_i
end

def find(key)
owner_json = self.get(path(key), {'accept' => 'application/json'}.merge(User.cp_oauth_header)).body
JSON.parse(owner_json).with_indifferent_access
end

def update(key, attrs)
owner = find(key)
owner.merge!(attrs)
self.put(path(key), JSON.generate(owner), self.default_headers).body
end

def import(organization_name, path_to_file, options)
path = join_path(path(organization_name), 'imports')

query_params = {}
query_params[:force] = true if options[:force] == "true"
unless query_params.empty?
path << "?" << query_params.to_param
end

self.post(path, {:import => File.new(path_to_file, 'rb')}, self.default_headers.except('content-type'))
end

def destroy_imports(organization_name, wait_until_complete = false)
response_json = self.delete(join_path(path(organization_name), 'imports'), self.default_headers)
response = JSON.parse(response_json).with_indifferent_access
if wait_until_complete && response['state'] == 'CREATED'
while !response['state'].nil? && response['state'] != 'FINISHED' && response['state'] != 'ERROR'
path = join_path('candlepin', response['statusPath'][1..-1])
response_json = self.get(path, self.default_headers)
response = JSON.parse(response_json).with_indifferent_access
end
end

response
end

def imports(organization_name)
imports_json = self.get(join_path(path(organization_name), 'imports'), self.default_headers)
Util::Data.array_with_indifferent_access JSON.parse(imports_json)
end

def pools(key, filter = {})
if key
json_str = self.get(join_path(path(key), 'pools') + hash_to_query(filter), self.default_headers).body
else
json_str = self.get(join_path('candlepin', 'pools') + hash_to_query(filter), self.default_headers).body
end
Util::Data.array_with_indifferent_access JSON.parse(json_str)
end

def statistics(key)
json_str = self.get(join_path(path(key), 'statistics'), self.default_headers).body
Util::Data.array_with_indifferent_access JSON.parse(json_str)
end

def generate_ueber_cert(key)
ueber_cert_json = self.post(join_path(path(key), "uebercert"), {}.to_json, self.default_headers).body
JSON.parse(ueber_cert_json).with_indifferent_access
end

def get_ueber_cert(key)
ueber_cert_json = self.get(join_path(path(key), "uebercert"), {'accept' => 'application/json'}.merge(User.cp_oauth_header)).body
JSON.parse(ueber_cert_json).with_indifferent_access
end

def get_ueber_cert_pkcs12(key, name = nil, password = nil)
certs = get_ueber_cert(key)
c = OpenSSL::X509::Certificate.new certs["cert"]
p = OpenSSL::PKey::RSA.new certs["key"]
OpenSSL::PKCS12.create(password, name, p, c, nil, "PBE-SHA1-3DES", "PBE-SHA1-3DES")
end

def events(key)
response = self.get(join_path(path(key), 'events'), self.default_headers).body
Util::Data.array_with_indifferent_access JSON.parse(response)
end

def service_levels(uuid)
response = Candlepin::CandlepinResource.get(join_path(path(uuid), 'servicelevels'), self.default_headers).body
if response.empty?
return []
else
JSON.parse(response)
end
end

def auto_attach(key)
response = self.post(join_path(path(key), 'entitlements'), "", self.default_headers).body
if response.empty?
return nil
else
JSON.parse(response)
end
end

def path(id = nil)
"/candlepin/owners/#{id}"
end
end
end

class Environment < CandlepinResource
class << self

def find(id)
JSON.parse(self.get(path(id), self.default_headers).body).with_indifferent_access
end

def all
JSON.parse(self.get(path, self.default_headers).body).collect{|a| a.with_indifferent_access}
end

def create(owner_id, id, name, description)
attrs = {:id => id, :name => name, :description => description}
path = "/candlepin/owners/#{owner_id}/environments"
environment_json = self.post(path, attrs.to_json, self.default_headers).body
JSON.parse(environment_json).with_indifferent_access
end

def destroy(id)
self.delete(path(id), User.cp_oauth_header).code.to_i
end

def path(id = '')
"/candlepin/environments/#{id}"
end

def add_content(env_id, content_ids)
path = self.path(env_id) + "/content"
params = content_ids.map {|content_id| {:contentId => content_id} }
JSON.parse(self.post(path, params.to_json, self.default_headers).body).with_indifferent_access
end

def delete_content(env_id, content_ids)
path = self.path(env_id) + "/content"
params = content_ids.map {|content_id| {:content => content_id}.to_param }.join("&")
self.delete("#{path}?#{params}", self.default_headers).code.to_i
end
end
end

class CPUser < CandlepinResource
class << self
def create(attrs)
JSON.parse(self.post(path, JSON.generate(attrs), self.default_headers).body).with_indifferent_access
end

def path(id = nil)
"/candlepin/users/#{id}"
end
end
end

class Pool < CandlepinResource
class << self
def find(pool_id)
pool_json = self.get(path(pool_id), self.default_headers).body
fail ArgumentError, "pool id cannot contain ?" if pool_id["?"]
JSON.parse(pool_json).with_indifferent_access
end

def get_for_owner(owner_key)
pools_json = self.get("/candlepin/owners/#{owner_key}/pools", self.default_headers).body
JSON.parse(pools_json)
end

def destroy(id)
fail ArgumentError, "pool id has to be specified" unless id
self.delete(path(id), self.default_headers).code.to_i
end

def all
pools_json = self.get(path, self.default_headers).body
JSON.parse(pools_json)
end

def path(id = nil)
"/candlepin/pools/#{id}"
end
end
end

class Content < CandlepinResource
class << self
def create(attrs)
JSON.parse(self.post(path, JSON.generate(attrs), self.default_headers).body).with_indifferent_access
end

def get(id)
content_json = super(path(id), self.default_headers).body
JSON.parse(content_json).with_indifferent_access
end

def all
content_json = Candlepin::CandlepinResource.get(path, self.default_headers).body
JSON.parse(content_json)
end

def destroy(id)
fail ArgumentError, "content id has to be specified" unless id
self.delete(path(id), self.default_headers).code.to_i
end

def update(attrs)
JSON.parse(self.put(path(attrs[:id] || attrs['id']), JSON.generate(attrs), self.default_headers).body).with_indifferent_access
end

def path(id = nil)
"/candlepin/content/#{id}"
end
end
end

class Subscription < CandlepinResource
class << self

def destroy(subscription_id)
fail ArgumentError, "subscription id has to be specified" unless subscription_id
self.delete(path(subscription_id), self.default_headers).code.to_i
end

def get(id = nil)
content_json = super(path(id), self.default_headers).body
JSON.parse(content_json)
end

def create_for_owner(owner_key, attrs)
subscription = self.post("/candlepin/owners/#{owner_key}/subscriptions", attrs.to_json, self.default_headers).body
self.put("/candlepin/owners/#{owner_key}/subscriptions", {}.to_json, self.default_headers).body
subscription
end

def get_for_owner(owner_key)
content_json = Candlepin::CandlepinResource.get("/candlepin/owners/#{owner_key}/subscriptions", self.default_headers).body
JSON.parse(content_json)
end

def refresh_for_owner(owner_key)
ret = self.put("/candlepin/owners/#{owner_key}/subscriptions", {}.to_json, self.default_headers).body
JSON.parse(ret).with_indifferent_access
end

def path(id = nil)
"/candlepin/subscriptions/#{id}"
end
end
end

class Job < CandlepinResource
class << self

NOT_FINISHED_STATES = %w[CREATED PENDING RUNNING] unless defined? NOT_FINISHED_STATES

def not_finished?(job)
NOT_FINISHED_STATES.include?(job[:state])
end

def get(id)
job_json = super(path(id), self.default_headers).body
job = JSON.parse(job_json)
job.with_indifferent_access
end

def path(id = nil)
"/candlepin/jobs/#{id}"
end
end
end

class Product < CandlepinResource
class << self

def all
JSON.parse(Candlepin::CandlepinResource.get(path, self.default_headers).body)
end

def create(attr)
JSON.parse(self.post(path, attr.to_json, self.default_headers).body).with_indifferent_access
end

def get(id = nil)
products_json = super(path(id), self.default_headers).body
products = JSON.parse(products_json)
products = [products] unless id.nil?
Util::Data.array_with_indifferent_access products
end

def _certificate_and_key(id, owner)
subscriptions_json = Candlepin::CandlepinResource.get("/candlepin/owners/#{owner}/subscriptions", self.default_headers).body
subscriptions = JSON.parse(subscriptions_json)

product_subscription = subscriptions.find do |sub|
sub["product"]["id"] == id ||
sub["providedProducts"].any? { |provided| provided["id"] == id } ||
sub["derivedProvidedProducts"].any? { |provided| provided["id"] == id }
end

if product_subscription
return product_subscription["certificate"]
end
end

def certificate(id, owner)
self._certificate_and_key(id, owner).try :[], 'cert'
end

def key(id, owner)
self._certificate_and_key(id, owner).try :[], 'key'
end

def destroy(product_id)
fail ArgumentError, "product id has to be specified" unless product_id
self.delete(path(product_id), self.default_headers).code.to_i
end

def add_content(product_id, content_id, enabled)
self.post(join_path(path(product_id), "content/#{content_id}?enabled=#{enabled}"), nil, self.default_headers).code.to_i
end

def remove_content(product_id, content_id)
self.delete(join_path(path(product_id), "content/#{content_id}"), self.default_headers).code.to_i
end

def create_unlimited_subscription(owner_key, product_id)
start_date ||= Date.today
# End it 100 years from now
end_date ||= start_date + 10_950

subscription = {
'startDate' => start_date,
'endDate' => end_date,
'quantity' => -1,
'accountNumber' => '',
'product' => { 'id' => product_id },
'providedProducts' => [],
'contractNumber' => ''
}
Candlepin::Subscription.create_for_owner owner_key, subscription
end

def pools(owner_key, product_id)
Candlepin::Pool.get_for_owner(owner_key).find_all {|pool| pool['productId'] == product_id }
end

def delete_subscriptions(owner_key, product_id)
update_subscriptions = false
subscriptions = Candlepin::Subscription.get_for_owner owner_key
subscriptions.each do |s|
products = ([s['product']] + s['providedProducts'])
products.each do |p|
if p['id'] == product_id
logger.debug "Deleting subscription: " + s.to_json
Candlepin::Subscription.destroy s['id']
update_subscriptions = true
end
end
end

if update_subscriptions
return Candlepin::Subscription.refresh_for_owner owner_key
else
return nil
end
end

def path(id = nil)
"/candlepin/products/#{id}"
end
end
end

class Entitlement < CandlepinResource
class << self
def regenerate_entitlement_certificates_for_product(product_id)
self.put("/candlepin/entitlements/product/#{product_id}", nil, self.default_headers).code.to_i
end

def get(id = nil)
json = Candlepin::CandlepinResource.get(path(id), self.default_headers).body
JSON.parse(json)
end

def path(id = nil)
"/candlepin/entitlements/#{id}"
end
end
end

class ActivationKey < CandlepinResource
class << self

def get(id = nil)
akeys_json = super(path(id), self.default_headers).body
akeys = JSON.parse(akeys_json)
akeys = [akeys] unless id.nil?
Util::Data.array_with_indifferent_access akeys
end

def create(name, owner_key)
url = "/candlepin/owners/#{owner_key}/activation_keys"
JSON.parse(self.post(url, {:name => name}.to_json, self.default_headers).body).with_indifferent_access
end

def update(id, release_version, service_level)
attrs = { :releaseVer => release_version, :serviceLevel => service_level }.delete_if { |k, v| v.nil? }
if attrs.empty?
return true
else
JSON.parse(self.put(path(id), attrs.to_json, self.default_headers).body).with_indifferent_access
end
end

def destroy(id)
fail(ArgumentError, "activation key id has to be specified") unless id
self.delete(path(id), self.default_headers).code.to_i
end

def pools(owner_key)
Candlepin::Owner.pools(owner_key)
end

def key_pools(id)
kp_json = Candlepin::CandlepinResource.get(join_path(path(id), "pools"), self.default_headers).body
key_pools = JSON.parse(kp_json)
Util::Data.array_with_indifferent_access key_pools
end

def add_pools(id, pool_id, quantity)
cppath = join_path(path(id), "pools/#{pool_id}")
quantity = Integer(quantity) rescue nil
if quantity && quantity > 0
cppath += "?quantity=#{quantity}" if quantity
end
pool = self.post(cppath, {}, self.default_headers)
JSON.parse(pool).with_indifferent_access
end

def remove_pools(id, pool_id)
pool = self.delete(join_path(path(id), "pools/#{pool_id}"), self.default_headers)
JSON.parse(pool).with_indifferent_access
end

def content_overrides(id)
result = Candlepin::CandlepinResource.get(join_path(path(id), 'content_overrides'), self.default_headers).body
Util::Data.array_with_indifferent_access(JSON.parse(result))
end

def update_content_override(id, content_label, name, value = nil)
attrs = [{ :contentLabel => content_label, :name => name }]
if value
attrs[0][:value] = value
result = Candlepin::CandlepinResource.put(join_path(path(id), 'content_overrides'),
attrs.to_json, self.default_headers)
else
client = Candlepin::CandlepinResource.rest_client(Net::HTTP::Delete, :delete,
join_path(path(id), 'content_overrides'))
client.options[:payload] = attrs.to_json
result = client.delete({:accept => :json, :content_type => :json}.merge(User.cp_oauth_header))
end
Util::Data.array_with_indifferent_access(JSON.parse(result))
end

def path(id = nil)
"/candlepin/activation_keys/#{id}"
end
end
end

end
end
end
(1-1/2)