Compare commits
11 Commits
feature-re
...
master
Author | SHA1 | Date | |
---|---|---|---|
a18207bd33 | |||
77419ccc8f | |||
7af27cdb72 | |||
6b9260af43 | |||
26d03ddfea | |||
a41ad51d04 | |||
c6f238ae01 | |||
91d06fa18c | |||
6301afec52 | |||
5232f2bebb | |||
3aae6b998a |
@ -26,3 +26,6 @@ This will allow you to restore a file on disk to the specific version that is st
|
|||||||
|
|
||||||
### Get
|
### Get
|
||||||
The utility will allow you to get the contents of a backup up file and have it output to the terminal.
|
The utility will allow you to get the contents of a backup up file and have it output to the terminal.
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
The utility allows you to delete entries/backups in the bucket if you choose to do so.
|
||||||
|
309
bucket-tool
Normal file → Executable file
309
bucket-tool
Normal file → Executable file
@ -6,44 +6,68 @@ require 'digest'
|
|||||||
|
|
||||||
# END: Requires
|
# END: Requires
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# BEGIN: Helper Functions
|
# BEGIN: Helper Functions
|
||||||
|
|
||||||
def usage
|
def usage
|
||||||
puts "#{__FILE__} [ACTION <arg>] [<flags>]
|
puts <<EOT
|
||||||
|
#{__FILE__} [ACTION <arg>] [<flags>]
|
||||||
|
|
||||||
Description:
|
Description:
|
||||||
This utlity is meant to be used to interact with & manage the filebucket on P4 nodes due to the
|
This utlity is meant to be used to interact with & manage the filebucket in the same capacity as
|
||||||
utility for that `puppet filebucket -l <action>` being nonfunctional.
|
the builtin filebucket utility. However, it does not handle any puppet tie ins.
|
||||||
|
|
||||||
This implements the same functionality (minus the puppet tie-in) and will allow the user to
|
I created this due to the puppet filebucket utility in my companies P4 envrionments not
|
||||||
search the filebucket and restore from it.
|
functioning
|
||||||
|
|
||||||
Actions:
|
Actions:
|
||||||
search <term> : Search for bucket entries matching a portion of the filepath
|
search <term> : Search for bucket entries matching a portion of the filepath
|
||||||
list : List all Bucket entries
|
list : List all Bucket entries
|
||||||
list-files : List all files/paths that have been backed up to the bucket
|
list-files : List all files/paths that have been backed up to the bucket
|
||||||
get <entry-hash> : Get the content of a specific entry (by hash)
|
get <entry-hash> : Get the content of a specific entry (by hash)
|
||||||
restore <value> : Restore previous state of file stored in bucket. Value can be hash or filename/filepath
|
restore <value> : Restore previous state of file stored in bucket. Value can be hash or filename/filepath
|
||||||
|
: Note: To restore to an alternate path you will need to provide the path via the -o flag
|
||||||
|
:
|
||||||
|
backup <file> : Backup the file to the bucket
|
||||||
|
delete <hash> : Delete the entry from the bucket
|
||||||
|
|
||||||
|
Restore Flags:
|
||||||
|
-o | --output-file <file> : Used to provide an alternate restoral path for the restore function
|
||||||
|
|
||||||
Global Flags:
|
Global Flags:
|
||||||
-d | --debug : Set debug flag
|
-d | --debug : Set debug flag
|
||||||
-h | --help : This help message
|
-h | --help : This help message
|
||||||
|
-b | --bucket <bucket-dir> : User specified bucket directory
|
||||||
|
|
||||||
|
Listing Filter Flags:
|
||||||
|
--from-date <DATE> : Filter listings from starting after this point (FORMAT: YYYY-MM-DD HH:MM:SS)
|
||||||
|
: EX:
|
||||||
|
: 1. --from-date 2023-05-10
|
||||||
|
: 2. --from-date "2023-05-10 13:23"
|
||||||
|
:
|
||||||
|
--to-date <DATE> : Filter listings from ending at this point (FORMAT: YYYY-MM-DD HH:MM:SS)
|
||||||
|
: EX:
|
||||||
|
: 1. --to-date 2024-05-10
|
||||||
|
: 2. --to-date "2024-05-10 05:27"
|
||||||
|
:
|
||||||
|
|
||||||
|
|
||||||
Info Format Flags:
|
Info Format Flags:
|
||||||
-i | --inline : Set the info format to inline (MTIME : HASH : FILENAME)
|
-i | --inline : Set the info format to inline (MTIME : HASH : FILENAME)
|
||||||
-l | --long : Set the info format to long
|
-l | --long : Set the info format to long
|
||||||
: Entry [HASH]:
|
: Entry [HASH]:
|
||||||
: Paths: path1,path2,...,pathn
|
: Paths: path1,path2,...,pathn
|
||||||
: MTIME: YYYY-MM-DD HH:MM:SS -####
|
: MTIME: YYYY-MM-DD HH:MM:SS -####
|
||||||
:
|
:
|
||||||
-c | --csv : Set the info format to csv ( MTIME,HASH,FILENAME1[;FILENAMEn] )
|
-c | --csv : Set the info format to csv ( MTIME,HASH,FILENAME1[;FILENAMEn] )
|
||||||
|
|
||||||
Author:
|
Author:
|
||||||
Name: Tristan Ancelet
|
Name: Tristan Ancelet
|
||||||
Email: tristan.ancelet@acumera.com
|
Email: tristan.ancelet@acumera.com
|
||||||
Phone (Work) #: +1 (337) 965-1855
|
Phone (Work) #: +1 (337) 965-1855
|
||||||
|
|
||||||
"
|
EOT
|
||||||
end
|
end
|
||||||
|
|
||||||
def log (message, level = 0)
|
def log (message, level = 0)
|
||||||
@ -180,14 +204,19 @@ end
|
|||||||
|
|
||||||
$CONFIG = Hash.new
|
$CONFIG = Hash.new
|
||||||
$CONFIG[:bucket_dir]=` #{puppet_exe} agent --configprint clientbucketdir `.strip()
|
$CONFIG[:bucket_dir]=` #{puppet_exe} agent --configprint clientbucketdir `.strip()
|
||||||
|
$CONFIG[:puppet_version]=` #{puppet_exe} --version `
|
||||||
$CONFIG[:action]=""
|
$CONFIG[:action]=""
|
||||||
$CONFIG[:search_term]=""
|
$CONFIG[:search_term]=""
|
||||||
$CONFIG[:log_file]=""
|
$CONFIG[:log_file]=""
|
||||||
|
$CONFIG[:alt_filepath]=""
|
||||||
$CONFIG[:info_format]="inline"
|
$CONFIG[:info_format]="inline"
|
||||||
|
$CONFIG[:from_time]=Time.at(0) # Beginning of epoch time
|
||||||
|
$CONFIG[:to_time]=Time.now # Today
|
||||||
File.open('/etc/hostname') do |file|
|
File.open('/etc/hostname') do |file|
|
||||||
$HOSTNAME=file.read().strip()
|
$HOSTNAME=file.read().strip()
|
||||||
end
|
end
|
||||||
FLAG_REGEX=/\-+\S+/
|
FLAG_REGEX=/^\-+\S+/
|
||||||
|
DATE_REGEX=/^(?<year>[0-9]{4})-(?<month>[0-9]{1,2})-(?<day>[0-9]{1,2})[[:space:]]*((?<hour>[0-9]{1,2}):(?<minute>[0-9]{1,2}):(?<second>[0-9]{1,2}))?$/
|
||||||
|
|
||||||
# END: Variables
|
# END: Variables
|
||||||
|
|
||||||
@ -207,7 +236,7 @@ if not (ARGV & ['-d', '--debug']).empty?
|
|||||||
end
|
end
|
||||||
i=0
|
i=0
|
||||||
case ARGV[i]
|
case ARGV[i]
|
||||||
when 'search', 'get', 'restore'
|
when 'search', 'get', 'restore', 'backup', 'delete'
|
||||||
$CONFIG[:action]=ARGV[i]
|
$CONFIG[:action]=ARGV[i]
|
||||||
log "$CONFIG[:action] was set to #{ARGV[i]}"
|
log "$CONFIG[:action] was set to #{ARGV[i]}"
|
||||||
log "user provided search action ARGV[i.next] == #{ARGV[i.next]}"
|
log "user provided search action ARGV[i.next] == #{ARGV[i.next]}"
|
||||||
@ -246,6 +275,57 @@ while i < ARGV.count
|
|||||||
$CONFIG[:info_format]='inline'
|
$CONFIG[:info_format]='inline'
|
||||||
log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}"
|
log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}"
|
||||||
i+=1
|
i+=1
|
||||||
|
when '-o', '--output-file'
|
||||||
|
log "user provided ARGV[i.next] == #{ARGV[i.next]}"
|
||||||
|
if ARGV[i.next] != "" and not ARGV[i.next] =~ FLAG_REGEX
|
||||||
|
$CONFIG[:alt_filepath]=File.expand_path(ARGV[i.next])
|
||||||
|
log "search_term was set to #{$CONFIG[:alt_filepath]}"
|
||||||
|
i+=2
|
||||||
|
else
|
||||||
|
puts "Flag[#{ARGV[i]}] : Argument[#{ARGV[i.next]}] : Either the argument was not provided or it was a flag"
|
||||||
|
usage
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
when '-d', '--debug'
|
||||||
|
log "#{ARGV[i]} as specified, and the $CONFIG[:debug] flag was already enabled"
|
||||||
|
i+=1
|
||||||
|
|
||||||
|
when '-b', '--bucket'
|
||||||
|
log "user provided ARGV[i.next] == #{ARGV[i.next]}"
|
||||||
|
if ARGV[i.next] != "" and not ARGV[i.next] =~ FLAG_REGEX
|
||||||
|
$CONFIG[:bucket_dir]=ARGV[i.next]
|
||||||
|
log "$CONFIG[:bucket_dir] was set to #{ARGV[i.next]}"
|
||||||
|
i+=2
|
||||||
|
else
|
||||||
|
puts "Flag[#{ARGV[i]}] : Argument[#{ARGV[i.next]}] : Either the argument was not provided or it was a flag"
|
||||||
|
usage
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
|
when '--to-date', '--from-date'
|
||||||
|
if ARGV[i.next] != "" and ARGV[i.next] =~ DATE_REGEX
|
||||||
|
date_matches = DATE_REGEX.match(ARGV[i.next]).named_captures.each_value.select{|val| not val.nil?}.map(&:to_i)
|
||||||
|
case ARGV[i]
|
||||||
|
when '--to-date'
|
||||||
|
$CONFIG[:to_time]=Time.new(*date_matches)
|
||||||
|
log "$CONFIG[:to_time] was set to #{$CONFIG[:to_time]}}"
|
||||||
|
when '--from-date'
|
||||||
|
$CONFIG[:from_time]=Time.new(*date_matches)
|
||||||
|
log "$CONFIG[:from_time] was set to #{$CONFIG[:from_time]}}"
|
||||||
|
end
|
||||||
|
i+=2
|
||||||
|
else
|
||||||
|
puts "Flag[#{ARGV[i]}] : Argument[#{ARGV[i.next]}] : Either the argument was not provided or it was a flag"
|
||||||
|
usage
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
|
when FLAG_REGEX
|
||||||
|
# Catch all to prevent user from specifying a non-accounted for flag
|
||||||
|
puts "#{ARGV[i]} is not a valid flag."
|
||||||
|
usage
|
||||||
|
exit
|
||||||
|
|
||||||
else
|
else
|
||||||
i+=1
|
i+=1
|
||||||
end
|
end
|
||||||
@ -257,6 +337,11 @@ if $CONFIG[:action] == ""
|
|||||||
puts "Action was not provided"
|
puts "Action was not provided"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not Dir.exist? $CONFIG[:bucket_dir]
|
||||||
|
puts "BucketDir[#{$CONFIG[:bucket_dir]}] Does not exist. Please check to make sure configuration is correct"
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
case $CONFIG[:action]
|
case $CONFIG[:action]
|
||||||
when 'search', 'get'
|
when 'search', 'get'
|
||||||
if $CONFIG[:search_term] == ""
|
if $CONFIG[:search_term] == ""
|
||||||
@ -265,6 +350,13 @@ case $CONFIG[:action]
|
|||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
|
|
||||||
|
when 'backup'
|
||||||
|
if not File.exist? $CONFIG[:search_term]
|
||||||
|
puts "File #{$CONFIG[:search_term]} does not exist. Please check to make sure that there are not typos and attempt the run again."
|
||||||
|
exit
|
||||||
|
else
|
||||||
|
$CONFIG[:search_term] = File.expand_path($CONFIG[:search_term])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
## END: Checks
|
## END: Checks
|
||||||
@ -325,6 +417,54 @@ class BucketEntry
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete
|
||||||
|
returncode = true
|
||||||
|
log "User has chosen to delete this BucketEntry"
|
||||||
|
|
||||||
|
Dir.chdir(@entry_dir){
|
||||||
|
log "Changed to #{@entry_dir} to delete children files"
|
||||||
|
Dir.children(@entry_dir).each{|file|
|
||||||
|
log "Deleting #{file}"
|
||||||
|
if File.unlink(file)
|
||||||
|
log "#{file} deleted"
|
||||||
|
else
|
||||||
|
puts "There was an issue deleting #{File.expand_path(file)}"
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log "Deleting #{@entry_dir}"
|
||||||
|
if Dir.delete(@entry_dir)
|
||||||
|
log "Deleted #{@entry_dir}"
|
||||||
|
else
|
||||||
|
puts "There was an issue when attempting to delete #{@entry_dir}"
|
||||||
|
end
|
||||||
|
|
||||||
|
dir = @entry_dir
|
||||||
|
|
||||||
|
log "Beginning to delete trailing dirs unless one has children other than those that make up #{File.dirname(@entry_dir)}"
|
||||||
|
for i in 1..8
|
||||||
|
dir = File.dirname(dir)
|
||||||
|
log "Beginning to delete #{dir}"
|
||||||
|
children = Dir.children(dir)
|
||||||
|
log "Dir[#{dir}] children found to be #{children.join(',')}"
|
||||||
|
if children.empty?
|
||||||
|
if Dir.delete(dir)
|
||||||
|
log "Deleted #{dir}"
|
||||||
|
else
|
||||||
|
puts "There was an issue when attempting to delete #{dir}"
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log "#{dir} showed to contain another child directory. Not deleting and breaking loop"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
returncode
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class Bucket
|
class Bucket
|
||||||
@ -350,8 +490,12 @@ class Bucket
|
|||||||
Dir["#{@bucketdir}/**/paths"].each.map{|path| File.dirname(path)}.each do |directory|
|
Dir["#{@bucketdir}/**/paths"].each.map{|path| File.dirname(path)}.each do |directory|
|
||||||
log "\"#{directory}\" was grabbed from bucket directory. Making new BucketEntry"
|
log "\"#{directory}\" was grabbed from bucket directory. Making new BucketEntry"
|
||||||
entry = BucketEntry.new(directory)
|
entry = BucketEntry.new(directory)
|
||||||
@entries[entry.hash]=entry
|
if entry.mtime <= $CONFIG[:to_time] and entry.mtime >= $CONFIG[:from_time]
|
||||||
log "BucketEntry[#{entry.hash}] was added to @entries Size=#{@entries.count()}"
|
@entries[entry.hash]=entry
|
||||||
|
log "BucketEntry[#{entry.hash}] was added to @entries Size=#{@entries.count()}"
|
||||||
|
else
|
||||||
|
log "Entry[#{entry.hash}] was filtered out by the user provided time constraints"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
log "Bucket[#{@bucketdir}] was loaded"
|
log "Bucket[#{@bucketdir}] was loaded"
|
||||||
end
|
end
|
||||||
@ -424,6 +568,10 @@ def get_entry_by_file (bucket, filenames)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if $CONFIG[:alt_filepath] != ""
|
||||||
|
return entry, $CONFIG[:alt_filepath]
|
||||||
|
end
|
||||||
|
|
||||||
if filename[0] != '/'
|
if filename[0] != '/'
|
||||||
filename = "/#{filename}"
|
filename = "/#{filename}"
|
||||||
end
|
end
|
||||||
@ -434,6 +582,12 @@ end
|
|||||||
def get_entry_by_hash (bucket)
|
def get_entry_by_hash (bucket)
|
||||||
if bucket.entries.has_key? $CONFIG[:search_term]
|
if bucket.entries.has_key? $CONFIG[:search_term]
|
||||||
entry = bucket.entries[$CONFIG[:search_term]]
|
entry = bucket.entries[$CONFIG[:search_term]]
|
||||||
|
|
||||||
|
if $CONFIG[:alt_filepath] != ""
|
||||||
|
log "$CONFIG[:alt_filepath] was set. Skipping prompts asking for filepaths"
|
||||||
|
return entry, $CONFIG[:alt_filepath]
|
||||||
|
end
|
||||||
|
|
||||||
filepath = ""
|
filepath = ""
|
||||||
if entry.filepaths.count == 1
|
if entry.filepaths.count == 1
|
||||||
filepath = entry.filepaths[0]
|
filepath = entry.filepaths[0]
|
||||||
@ -470,6 +624,108 @@ def restore_entry (bucket)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def backup_file (bucket)
|
||||||
|
hash=""
|
||||||
|
if Gem::Version.new($CONFIG[:puppet_version]) >= Gem::Version.new("7.0.0")
|
||||||
|
log "Puppet Version is +7.* so hashing algo will be SHA256"
|
||||||
|
File.open($CONFIG[:search_term],'r') do |file|
|
||||||
|
hash = Digest::SHA2.hexdigest file.read()
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
log "Puppet Version is not 7.* so hashing algo will be MD5"
|
||||||
|
File.open($CONFIG[:search_term],'r') do |file|
|
||||||
|
hash = Digest::MD5.hexdigest file.read()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
log "Hash for #{$CONFIG[:search_term]} was generated to be #{hash}"
|
||||||
|
|
||||||
|
if bucket.entries.has_key? hash
|
||||||
|
log "Hash was found to already be backed up to bucket"
|
||||||
|
puts "This file (hash: #{hash} has already been backed up in the bucket"
|
||||||
|
puts bucket.entries[hash].info
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
|
puppet_uid=0
|
||||||
|
puppet_gid=0
|
||||||
|
File.open('/etc/passwd','r') do |file|
|
||||||
|
log "Getting puppet UID & GID from /etc/passwd"
|
||||||
|
passd = file.read.split(/\n/).select{|line| line =~ /puppet.+/}.first
|
||||||
|
puppet_uid=Integer(passd.split(':')[2])
|
||||||
|
log "Retrieved Puppet UID as #{puppet_uid}"
|
||||||
|
puppet_gid=Integer(passd.split(':')[3])
|
||||||
|
log "Retrieved Puppet GID as #{puppet_gid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
log "Created preceeding directorys to hash directory"
|
||||||
|
dir=$CONFIG[:bucket_dir]
|
||||||
|
hash.chars[0,8].each{|char|
|
||||||
|
dir+="/#{char}"
|
||||||
|
log "Checking to make sure that #{dir} doesn't exist"
|
||||||
|
if not Dir.exist? dir
|
||||||
|
log "#{dir} didn't exist so creating the directory & changing the UID to #{puppet_uid} & GID #{puppet_gid}"
|
||||||
|
Dir.mkdir dir
|
||||||
|
File.chown(puppet_gid, puppet_uid, dir)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
entry_dir="#{dir}/#{hash}"
|
||||||
|
if not Dir.exist? entry_dir
|
||||||
|
log "#{entry_dir} didn't exist so creating the directory & changing the UID to #{puppet_uid} & GID #{puppet_gid}"
|
||||||
|
Dir.mkdir entry_dir
|
||||||
|
File.chown(puppet_gid, puppet_uid, entry_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
contents_path="#{entry_dir}/contents"
|
||||||
|
log "#{contents_path} will be created"
|
||||||
|
paths_path="#{entry_dir}/paths"
|
||||||
|
log "#{paths_path} will be created"
|
||||||
|
|
||||||
|
log "Creating #{contents_path}"
|
||||||
|
File.open(contents_path, 'w') do |contents_file|
|
||||||
|
log "Opened #{contents_path} to be written to"
|
||||||
|
File.open($CONFIG[:search_term], 'r') do |source_file|
|
||||||
|
log "Opened #{$CONFIG[:search_term]} to be read from"
|
||||||
|
contents_file.write(source_file.read)
|
||||||
|
contents_file.chown(puppet_gid, puppet_uid)
|
||||||
|
log "#{contents_path} was created & was chowned to UID to #{puppet_uid} & GID #{puppet_gid}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
log "Creating #{paths_path}"
|
||||||
|
File.open(paths_path, 'w') do |paths_file|
|
||||||
|
log "Opened #{paths_path} to be written to"
|
||||||
|
paths_file.write($CONFIG[:search_term])
|
||||||
|
log "Just wrote #{$CONFIG[:search_term]} to #{paths_path}"
|
||||||
|
paths_file.chown(puppet_gid, puppet_uid)
|
||||||
|
log "#{paths_path} was created & was chowned to UID to #{puppet_uid} & GID #{puppet_gid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "File #{$CONFIG[:search_term]} was backed up to #{entry_dir}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_entry (bucket)
|
||||||
|
if bucket.entries.has_key? $CONFIG[:search_term]
|
||||||
|
entry = bucket.entries[$CONFIG[:search_term]]
|
||||||
|
else
|
||||||
|
puts "BucketEntry[#{$CONFIG[:search_term]}] Does not exist. Please make sure you provided the correct hash value"
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Corresponding Entry: #{entry.info}"
|
||||||
|
if get_verification "Are you sure you want to delete BucketEntry[#{entry.hash}]? "
|
||||||
|
if get_verification "This cannot be undone. Are you sure you want to continue?: "
|
||||||
|
if entry.delete
|
||||||
|
puts "Ok. BucketEntry[#{entry.hash}] Has been deleted"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
# END: Work Functions
|
# END: Work Functions
|
||||||
|
|
||||||
|
|
||||||
@ -494,6 +750,11 @@ if __FILE__ == $0
|
|||||||
when 'restore'
|
when 'restore'
|
||||||
restore_entry bucket
|
restore_entry bucket
|
||||||
|
|
||||||
|
when 'backup'
|
||||||
|
backup_file bucket
|
||||||
|
|
||||||
|
when 'delete'
|
||||||
|
delete_entry bucket
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user