Partial merge to 2.6.2rc1 : Merge commit '66cf3a9' into next
There are test failures in commits following this one.
This commit is contained in:
Коммит
1a00c07971
|
@ -254,19 +254,6 @@ class Application
|
|||
def preinit
|
||||
end
|
||||
|
||||
def option_parser
|
||||
return @option_parser if defined?(@option_parser)
|
||||
|
||||
@option_parser = OptionParser.new(self.class.banner)
|
||||
|
||||
self.class.option_parser_commands.each do |options, fname|
|
||||
@option_parser.on(*options) do |value|
|
||||
self.send(fname, value)
|
||||
end
|
||||
end
|
||||
@option_parser
|
||||
end
|
||||
|
||||
def initialize(command_line = nil)
|
||||
require 'puppet/util/command_line'
|
||||
@command_line = command_line || Puppet::Util::CommandLine.new
|
||||
|
@ -323,20 +310,29 @@ class Application
|
|||
end
|
||||
|
||||
def parse_options
|
||||
# get all puppet options
|
||||
optparse_opt = []
|
||||
optparse_opt = Puppet.settings.optparse_addargs(optparse_opt)
|
||||
# Create an option parser
|
||||
option_parser = OptionParser.new(self.class.banner)
|
||||
|
||||
# convert them to OptionParser format
|
||||
optparse_opt.each do |option|
|
||||
self.option_parser.on(*option) do |arg|
|
||||
# Add all global options to it.
|
||||
Puppet.settings.optparse_addargs([]).each do |option|
|
||||
option_parser.on(*option) do |arg|
|
||||
handlearg(option[0], arg)
|
||||
end
|
||||
end
|
||||
|
||||
# scan command line argument
|
||||
# Add options that are local to this application, which were
|
||||
# created using the "option()" metaprogramming method. If there
|
||||
# are any conflicts, this application's options will be favored.
|
||||
self.class.option_parser_commands.each do |options, fname|
|
||||
option_parser.on(*options) do |value|
|
||||
# Call the method that "option()" created.
|
||||
self.send(fname, value)
|
||||
end
|
||||
end
|
||||
|
||||
# scan command line.
|
||||
begin
|
||||
self.option_parser.parse!(self.command_line.args)
|
||||
option_parser.parse!(self.command_line.args)
|
||||
rescue OptionParser::ParseError => detail
|
||||
$stderr.puts detail
|
||||
$stderr.puts "Try 'puppet #{command_line.subcommand_name} --help'"
|
||||
|
|
|
@ -268,7 +268,7 @@ module Puppet
|
|||
|
||||
setdefaults(
|
||||
:ca,
|
||||
:ca_name => ["$certname", "The name to use the Certificate Authority certificate."],
|
||||
:ca_name => ["Puppet CA: $certname", "The name to use the Certificate Authority certificate."],
|
||||
:cadir => { :default => "$ssldir/ca",
|
||||
:owner => "service",
|
||||
:group => "service",
|
||||
|
|
|
@ -24,9 +24,7 @@ Puppet.features.add(:rails) do
|
|||
end
|
||||
end
|
||||
|
||||
if ! (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR))
|
||||
false
|
||||
elsif ! (::ActiveRecord::VERSION::MAJOR == 2 and ::ActiveRecord::VERSION::MINOR >= 1)
|
||||
unless (Puppet::Util.activerecord_version >= 2.1)
|
||||
Puppet.info "ActiveRecord 2.1 or later required for StoreConfigs"
|
||||
false
|
||||
else
|
||||
|
|
|
@ -61,6 +61,13 @@ require 'puppet/provider/parsedfile'
|
|||
Dir.mkdir(dir, dir_perm)
|
||||
File.chown(uid, nil, dir)
|
||||
end
|
||||
|
||||
# ParsedFile usually calls backup_target much later in the flush process,
|
||||
# but our SUID makes that fail to open filebucket files for writing.
|
||||
# Fortunately, there's already logic to make sure it only ever happens once,
|
||||
# so calling it here supresses the later attempt by our superclass's flush method.
|
||||
self.class.backup_target(target)
|
||||
|
||||
Puppet::Util::SUIDManager.asuser(@resource.should(:user)) { super }
|
||||
File.chown(uid, nil, target)
|
||||
File.chmod(file_perm, target)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
require 'facter'
|
||||
require 'puppet'
|
||||
require 'logger'
|
||||
|
||||
module Puppet::Rails
|
||||
TIME_DEBUG = true
|
||||
|
@ -22,9 +23,8 @@ module Puppet::Rails
|
|||
ActiveRecord::Base.logger.level = Logger::DEBUG
|
||||
end
|
||||
|
||||
if (::ActiveRecord::VERSION::MAJOR == 2 and ::ActiveRecord::VERSION::MINOR <= 1)
|
||||
ActiveRecord::Base.allow_concurrency = true
|
||||
end
|
||||
# As of ActiveRecord 2.2 allow_concurrency has been deprecated and no longer has any effect.
|
||||
ActiveRecord::Base.allow_concurrency = true if Puppet::Util.activerecord_version < 2.2
|
||||
|
||||
ActiveRecord::Base.verify_active_connections!
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ type = Puppet::Util::Reference.newreference :type, :doc => "All Puppet resource
|
|||
Puppet::Type.eachtype { |type|
|
||||
next if type.name == :puppet
|
||||
next if type.name == :component
|
||||
next if type.name == :whit
|
||||
types[type.name] = type
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class Puppet::SSL::CertificateRequest < Puppet::SSL::Base
|
|||
# Support either an actual SSL key, or a Puppet key.
|
||||
key = key.content if key.is_a?(Puppet::SSL::Key)
|
||||
|
||||
# If we're a CSR for the CA, then use the real certname, rather than the
|
||||
# If we're a CSR for the CA, then use the real ca_name, rather than the
|
||||
# fake 'ca' name. This is mostly for backward compatibility with 0.24.x,
|
||||
# but it's also just a good idea.
|
||||
common_name = name == Puppet::SSL::CA_NAME ? Puppet.settings[:ca_name] : name
|
||||
|
|
|
@ -147,21 +147,19 @@ class Puppet::SSLCertificates::CA
|
|||
|
||||
# Create the root certificate.
|
||||
def mkrootcert
|
||||
# Make the root cert's name the FQDN of the host running the CA.
|
||||
name = Facter["hostname"].value
|
||||
# Make the root cert's name "Puppet CA: " plus the FQDN of the host running the CA.
|
||||
name = "Puppet CA: #{Facter["hostname"].value}"
|
||||
if domain = Facter["domain"].value
|
||||
name += ".#{domain}"
|
||||
end
|
||||
|
||||
cert = Certificate.new(
|
||||
|
||||
cert = Certificate.new(
|
||||
:name => name,
|
||||
:cert => @config[:cacert],
|
||||
:encrypt => @config[:capass],
|
||||
:key => @config[:cakey],
|
||||
:selfsign => true,
|
||||
:ttl => ttl,
|
||||
|
||||
:type => :ca
|
||||
)
|
||||
|
||||
|
@ -241,19 +239,15 @@ class Puppet::SSLCertificates::CA
|
|||
f << "%04X" % (serial + 1)
|
||||
}
|
||||
|
||||
|
||||
newcert = Puppet::SSLCertificates.mkcert(
|
||||
|
||||
newcert = Puppet::SSLCertificates.mkcert(
|
||||
:type => :server,
|
||||
:name => csr.subject,
|
||||
:ttl => ttl,
|
||||
:issuer => @cert,
|
||||
:serial => serial,
|
||||
|
||||
:publickey => csr.public_key
|
||||
)
|
||||
|
||||
|
||||
sign_with_key(newcert)
|
||||
|
||||
self.storeclientcert(newcert)
|
||||
|
|
|
@ -4,4 +4,8 @@ Puppet::Type.newtype(:whit) do
|
|||
newparam :name do
|
||||
desc "The name of the whit, because it must have one."
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Class[#{name}]"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,14 @@ module Util
|
|||
# Create a hash to store the different sync objects.
|
||||
@@syncresources = {}
|
||||
|
||||
def self.activerecord_version
|
||||
if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR))
|
||||
([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f)
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
# Return the sync object associated with a given resource.
|
||||
def self.sync(resource)
|
||||
@@syncresources[resource] ||= Sync.new
|
||||
|
|
|
@ -45,4 +45,11 @@ describe Puppet::Application::Doc do
|
|||
Dir.chdir(old_dir)
|
||||
end
|
||||
end
|
||||
|
||||
it "should respect the -o option" do
|
||||
puppetdoc = Puppet::Application[:doc]
|
||||
puppetdoc.command_line.stubs(:args).returns(['foo', '-o', 'bar'])
|
||||
puppetdoc.parse_options
|
||||
puppetdoc.options[:outputdir].should == 'bar'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -227,7 +227,7 @@ describe "Puppet defaults" do
|
|||
|
||||
it "should have a :caname setting that defaults to the cert name" do
|
||||
Puppet.settings[:certname] = "foo"
|
||||
Puppet.settings[:ca_name].should == "foo"
|
||||
Puppet.settings[:ca_name].should == "Puppet CA: foo"
|
||||
end
|
||||
|
||||
it "should have a 'prerun_command' that defaults to the empty string" do
|
||||
|
|
|
@ -191,24 +191,17 @@ describe Puppet::Application do
|
|||
Puppet.settings.stubs(:optparse_addargs).returns([])
|
||||
end
|
||||
|
||||
it "should create a new option parser when needed" do
|
||||
option_parser = stub "option parser"
|
||||
option_parser.stubs(:on)
|
||||
OptionParser.expects(:new).returns(option_parser).once
|
||||
@app.option_parser.should == option_parser
|
||||
@app.option_parser.should == option_parser
|
||||
end
|
||||
|
||||
it "should pass the banner to the option parser" do
|
||||
option_parser = stub "option parser"
|
||||
option_parser.stubs(:on)
|
||||
option_parser.stubs(:parse!)
|
||||
@app.class.instance_eval do
|
||||
banner "banner"
|
||||
end
|
||||
|
||||
OptionParser.expects(:new).with("banner").returns(option_parser)
|
||||
|
||||
@app.option_parser
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
it "should get options from Puppet.settings.optparse_addargs" do
|
||||
|
@ -219,15 +212,14 @@ describe Puppet::Application do
|
|||
|
||||
it "should add Puppet.settings options to OptionParser" do
|
||||
Puppet.settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option"]])
|
||||
|
||||
@app.option_parser.expects(:on).with { |*arg| arg == ["--option","-o", "Funny Option"] }
|
||||
|
||||
Puppet.settings.expects(:handlearg).with("--option", 'true')
|
||||
@app.command_line.stubs(:args).returns(["--option"])
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
it "should ask OptionParser to parse the command-line argument" do
|
||||
@app.command_line.stubs(:args).returns(%w{ fake args })
|
||||
@app.option_parser.expects(:parse!).with(%w{ fake args })
|
||||
OptionParser.any_instance.expects(:parse!).with(%w{ fake args })
|
||||
|
||||
@app.parse_options
|
||||
end
|
||||
|
@ -258,31 +250,30 @@ describe Puppet::Application do
|
|||
|
||||
describe "when dealing with an argument not declared directly by the application" do
|
||||
it "should pass it to handle_unknown if this method exists" do
|
||||
Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled"]])
|
||||
@app.option_parser.stubs(:on).yields("value")
|
||||
Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled", :REQUIRED]])
|
||||
|
||||
@app.expects(:handle_unknown).with("--not-handled", "value").returns(true)
|
||||
|
||||
@app.command_line.stubs(:args).returns(["--not-handled", "value"])
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
it "should pass it to Puppet.settings if handle_unknown says so" do
|
||||
Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet"]])
|
||||
@app.option_parser.stubs(:on).yields("value")
|
||||
Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet", :REQUIRED]])
|
||||
|
||||
@app.stubs(:handle_unknown).with("--topuppet", "value").returns(false)
|
||||
|
||||
Puppet.settings.expects(:handlearg).with("--topuppet", "value")
|
||||
@app.command_line.stubs(:args).returns(["--topuppet", "value"])
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
it "should pass it to Puppet.settings if there is no handle_unknown method" do
|
||||
Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet"]])
|
||||
@app.option_parser.stubs(:on).yields("value")
|
||||
Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet", :REQUIRED]])
|
||||
|
||||
@app.stubs(:respond_to?).returns(false)
|
||||
|
||||
Puppet.settings.expects(:handlearg).with("--topuppet", "value")
|
||||
@app.command_line.stubs(:args).returns(["--topuppet", "value"])
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
|
@ -310,7 +301,7 @@ describe Puppet::Application do
|
|||
|
||||
it "should exit if OptionParser raises an error" do
|
||||
$stderr.stubs(:puts)
|
||||
@app.option_parser.stubs(:parse!).raises(OptionParser::ParseError.new("blah blah"))
|
||||
OptionParser.any_instance.stubs(:parse!).raises(OptionParser::ParseError.new("blah blah"))
|
||||
|
||||
@app.expects(:exit)
|
||||
|
||||
|
@ -478,7 +469,7 @@ describe Puppet::Application do
|
|||
@app.class.option("--[no-]test3","-t") do
|
||||
end
|
||||
|
||||
@app.option_parser
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
it "should pass a block that calls our defined method" do
|
||||
|
@ -490,7 +481,7 @@ describe Puppet::Application do
|
|||
@app.class.option("--test4","-t") do
|
||||
end
|
||||
|
||||
@app.option_parser
|
||||
@app.parse_options
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -501,7 +492,7 @@ describe Puppet::Application do
|
|||
|
||||
@app.class.option("--test4","-t")
|
||||
|
||||
@app.option_parser
|
||||
@app.parse_options
|
||||
end
|
||||
|
||||
it "should give to OptionParser a block that adds the the value to the options array" do
|
||||
|
@ -512,7 +503,7 @@ describe Puppet::Application do
|
|||
|
||||
@app.class.option("--test4","-t")
|
||||
|
||||
@app.option_parser
|
||||
@app.parse_options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,14 +47,20 @@ describe Puppet::Rails, "when initializing any connection" do
|
|||
Puppet::Rails.connect
|
||||
end
|
||||
|
||||
describe "on ActiveRecord 2.1.x" do
|
||||
confine("ActiveRecord 2.1.x") { ::ActiveRecord::VERSION::MAJOR == 2 and ::ActiveRecord::VERSION::MINOR <= 1 }
|
||||
|
||||
it "should set ActiveRecord::Base.allow_concurrency" do
|
||||
describe "ActiveRecord Version" do
|
||||
it "should set ActiveRecord::Base.allow_concurrency if ActiveRecord is 2.1" do
|
||||
Puppet::Util.stubs(:activerecord_version).returns(2.1)
|
||||
ActiveRecord::Base.expects(:allow_concurrency=).with(true)
|
||||
|
||||
Puppet::Rails.connect
|
||||
end
|
||||
|
||||
it "should not set ActiveRecord::Base.allow_concurrency if ActiveRecord is >= 2.2" do
|
||||
Puppet::Util.stubs(:activerecord_version).returns(2.2)
|
||||
ActiveRecord::Base.expects(:allow_concurrency=).never
|
||||
|
||||
Puppet::Rails.connect
|
||||
end
|
||||
end
|
||||
|
||||
it "should call ActiveRecord::Base.verify_active_connections!" do
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env ruby
|
||||
require File.dirname(__FILE__) + '/../../spec_helper'
|
||||
|
||||
require 'puppet'
|
||||
require 'puppet/sslcertificates'
|
||||
require 'puppet/sslcertificates/ca'
|
||||
|
||||
describe Puppet::SSLCertificates::CA do
|
||||
before :all do
|
||||
@hosts = %w{host.domain.com Other.Testing.Com}
|
||||
end
|
||||
|
||||
before :each do
|
||||
Puppet::Util::SUIDManager.stubs(:asuser).yields
|
||||
file = Tempfile.new("ca_testing")
|
||||
@dir = file.path
|
||||
file.delete
|
||||
|
||||
Puppet.settings[:confdir] = @dir
|
||||
Puppet.settings[:vardir] = @dir
|
||||
|
||||
@ca = Puppet::SSLCertificates::CA.new
|
||||
end
|
||||
|
||||
after :each do
|
||||
system("rm -rf #{@dir}")
|
||||
end
|
||||
|
||||
describe 'when cleaning' do
|
||||
it 'should remove associated files' do
|
||||
dirs = [:csrdir, :signeddir, :publickeydir, :privatekeydir, :certdir]
|
||||
|
||||
@hosts.each do |host|
|
||||
files = []
|
||||
dirs.each do |dir|
|
||||
dir = Puppet[dir]
|
||||
|
||||
# Case insensitivity is handled through downcasing
|
||||
file = File.join(dir, host.downcase + '.pem')
|
||||
|
||||
File.open(file, "w") do |f|
|
||||
f.puts "testing"
|
||||
end
|
||||
|
||||
files << file
|
||||
end
|
||||
|
||||
lambda { @ca.clean(host) }.should_not raise_error
|
||||
|
||||
files.reject {|f| ! File.exists?(f)}.should be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when mapping hosts to files' do
|
||||
it 'should correctly return the certfile' do
|
||||
@hosts.each do |host|
|
||||
value = nil
|
||||
lambda { value = @ca.host2certfile host }.should_not raise_error
|
||||
|
||||
File.join(Puppet[:signeddir], host.downcase + '.pem').should == value
|
||||
end
|
||||
end
|
||||
|
||||
it 'should correctly return the csrfile' do
|
||||
@hosts.each do |host|
|
||||
value = nil
|
||||
lambda { value = @ca.host2csrfile host }.should_not raise_error
|
||||
|
||||
File.join(Puppet[:csrdir], host.downcase + '.pem').should == value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when listing' do
|
||||
it 'should find all csr' do
|
||||
list = []
|
||||
|
||||
# Make some fake CSRs
|
||||
@hosts.each do |host|
|
||||
file = File.join(Puppet[:csrdir], host.downcase + '.pem')
|
||||
File.open(file, 'w') { |f| f.puts "yay" }
|
||||
list << host.downcase
|
||||
end
|
||||
|
||||
@ca.list.sort.should == list.sort
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when creating a root certificate' do
|
||||
before :each do
|
||||
lambda { @ca.mkrootcert }.should_not raise_exception
|
||||
end
|
||||
|
||||
it 'should store the public key' do
|
||||
File.exists?(Puppet[:capub]).should be_true
|
||||
end
|
||||
|
||||
it 'should prepend "Puppet CA: " to the fqdn as the ca_name by default' do
|
||||
host_mock_fact = mock()
|
||||
host_mock_fact.expects(:value).returns('myhost')
|
||||
domain_mock_fact = mock()
|
||||
domain_mock_fact.expects(:value).returns('puppetlabs.lan')
|
||||
Facter.stubs(:[]).with('hostname').returns(host_mock_fact)
|
||||
Facter.stubs(:[]).with('domain').returns(domain_mock_fact)
|
||||
|
||||
@ca.mkrootcert.name.should == 'Puppet CA: myhost.puppetlabs.lan'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require File.dirname(__FILE__) + '/../../spec_helper'
|
||||
|
||||
whit = Puppet::Type.type(:whit).new(:name => "Foo::Bar")
|
||||
|
||||
describe whit do
|
||||
it "should stringify as though it were the class it represents" do
|
||||
whit.to_s.should == "Class[Foo::Bar]"
|
||||
end
|
||||
end
|
|
@ -1,87 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require File.dirname(__FILE__) + '/../lib/puppettest'
|
||||
|
||||
require 'puppet'
|
||||
require 'puppet/sslcertificates/ca.rb'
|
||||
require 'puppettest'
|
||||
require 'puppettest/certificates'
|
||||
require 'mocha'
|
||||
|
||||
class TestCA < Test::Unit::TestCase
|
||||
include PuppetTest
|
||||
|
||||
def setup
|
||||
super
|
||||
Puppet::Util::SUIDManager.stubs(:asuser).yields
|
||||
end
|
||||
|
||||
def hosts
|
||||
%w{host.domain.com Other.Testing.Com}
|
||||
end
|
||||
def mkca
|
||||
Puppet::SSLCertificates::CA.new
|
||||
end
|
||||
|
||||
def test_clean
|
||||
dirs = [:csrdir, :signeddir, :publickeydir, :privatekeydir, :certdir]
|
||||
ca = mkca
|
||||
|
||||
hosts.each do |host|
|
||||
files = []
|
||||
dirs.each do |dir|
|
||||
dir = Puppet[dir]
|
||||
# We handle case insensitivity through downcasing
|
||||
file = File.join(dir, host.downcase + ".pem")
|
||||
File.open(file, "w") do |f|
|
||||
f.puts "testing"
|
||||
end
|
||||
files << file
|
||||
end
|
||||
assert_nothing_raised do
|
||||
ca.clean(host)
|
||||
end
|
||||
files.each do |f|
|
||||
assert(! FileTest.exists?(f), "File #{f} was not deleted")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_host2Xfile
|
||||
ca = mkca
|
||||
hosts.each do |host|
|
||||
{:signeddir => :host2certfile, :csrdir => :host2csrfile}.each do |dir, method|
|
||||
val = nil
|
||||
assert_nothing_raised do
|
||||
val = ca.send(method, host)
|
||||
end
|
||||
assert_equal(File.join(Puppet[dir], host.downcase + ".pem"), val,
|
||||
"incorrect response from #{method}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_list
|
||||
ca = mkca
|
||||
# Make a fake csr
|
||||
dir = Puppet[:csrdir]
|
||||
list = []
|
||||
hosts.each do |host|
|
||||
file = File.join(dir, host.downcase + ".pem")
|
||||
File.open(file, "w") { |f| f.puts "yay" }
|
||||
list << host.downcase
|
||||
end
|
||||
|
||||
assert_equal(list.sort, ca.list.sort, "list was not correct")
|
||||
end
|
||||
|
||||
# #142 - test storing the public key
|
||||
def test_store_public_key
|
||||
ca = mkca
|
||||
assert_nothing_raised do
|
||||
ca.mkrootcert
|
||||
end
|
||||
assert(FileTest.exists?(Puppet[:capub]), "did not store public key")
|
||||
end
|
||||
end
|
||||
|
Загрузка…
Ссылка в новой задаче