create external lib for all actions
move all code to an external library
This commit is contained in:
257
Rakefile
257
Rakefile
@@ -1,55 +1,14 @@
|
||||
require 'yaml'
|
||||
require 'rubygems'
|
||||
|
||||
|
||||
def cmd_system (cmd)
|
||||
result = system cmd
|
||||
raise(RuntimeError, $?) unless $?.success?
|
||||
result
|
||||
end
|
||||
|
||||
def git_cmd(cmd)
|
||||
command = 'git ' + cmd
|
||||
Open3.popen3(*command) do |i, o, e, t|
|
||||
raise StandardError, e.read unless (t ? t.value : $?).success?
|
||||
o.read.split("\n")
|
||||
end
|
||||
end
|
||||
|
||||
def on_box (box, cmd)
|
||||
cmd_system("vagrant ssh #{box} -c '#{cmd}'")
|
||||
end
|
||||
|
||||
def deploy_two_node
|
||||
require 'vagrant'
|
||||
env = Vagrant::Environment.new(:cwd => base_dir, :ui_class => Vagrant::UI::Colored)
|
||||
build(:openstack_controller, env)
|
||||
build(:compute1, env)
|
||||
end
|
||||
|
||||
def base_dir
|
||||
File.expand_path(File.dirname(__FILE__))
|
||||
end
|
||||
|
||||
# bring vagrant vm with image name up
|
||||
def build(instance, env)
|
||||
unless vm = env.vms[instance]
|
||||
puts "invalid VM: #{instance}"
|
||||
else
|
||||
if vm.created?
|
||||
puts "VM: #{instance} was already created"
|
||||
else
|
||||
# be very fault tolerant :)
|
||||
begin
|
||||
# this will always fail
|
||||
vm.up(:provision => true)
|
||||
rescue Exception => e
|
||||
puts e.class
|
||||
puts e
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
require File.join(base_dir, 'lib', 'puppetlabs', 'os_tester')
|
||||
|
||||
include Puppetlabs::OsTester
|
||||
|
||||
|
||||
namespace :openstack do
|
||||
|
||||
@@ -58,10 +17,9 @@ namespace :openstack do
|
||||
cmd_system('librarian-puppet install')
|
||||
end
|
||||
|
||||
desc 'destroy all vms'
|
||||
task 'destroy' do
|
||||
puts "About to destroy all vms..."
|
||||
cmd_system('vagrant destroy -f')
|
||||
puts "Destroyed all vms"
|
||||
destroy_all_vms
|
||||
end
|
||||
|
||||
desc 'deploys the entire environment'
|
||||
@@ -72,54 +30,23 @@ namespace :openstack do
|
||||
end
|
||||
|
||||
|
||||
remote_name = 'bodepd'
|
||||
|
||||
namespace :git do
|
||||
|
||||
cwd = base_dir
|
||||
|
||||
desc 'for all repos in the module directory, add a read/write remote'
|
||||
desc 'for all repos in the module directory, add a read/write remote (hardcoded to bodepd)'
|
||||
task :dev_setup do
|
||||
|
||||
each_repo do |module_name|
|
||||
# need to handle more failure cases
|
||||
remotes = git_cmd('remote')
|
||||
if remotes.include?(remote_name)
|
||||
puts "Did not have to add remote #{remote_name} to #{module_name}"
|
||||
elsif ! remotes.include?('origin')
|
||||
raise(Exception, "Repo #{module_name} has no remote called origin, failing")
|
||||
else
|
||||
remote_url = git_cmd('remote show origin').detect {|x| x =~ /\s+Push\s+URL: / }
|
||||
if remote_url =~ /(git|https?):\/\/(.+)\/(.+)?\/(.+)/
|
||||
url = "git@#{$2}:#{remote_name}/#{$4}"
|
||||
else
|
||||
puts "remote_url #{remote_url} did not have the expected format. weird..."
|
||||
end
|
||||
puts "Adding remote #{remote_name} as #{url}"
|
||||
git_cmd("remote add #{remote_name} #{url}")
|
||||
end
|
||||
end
|
||||
dev_setup('bodped')
|
||||
end
|
||||
|
||||
desc 'pull the latest version of all code'
|
||||
task :pull_all do
|
||||
each_repo do |module_name|
|
||||
puts "Pulling repo: #{module_name}"
|
||||
puts ' ' + git_cmd('pull').join("\n ")
|
||||
end
|
||||
pull_all
|
||||
end
|
||||
|
||||
desc 'shows the current state of code that has not been commited'
|
||||
task :status_all do
|
||||
each_repo do |module_name|
|
||||
status = git_cmd('status')
|
||||
if status.include?('nothing to commit (working directory clean)')
|
||||
puts "Module #{module_name} has not changed" if verbose
|
||||
else
|
||||
puts "Uncommitted changes for: #{module_name}"
|
||||
puts " #{status.join("\n ")}"
|
||||
end
|
||||
end
|
||||
status_all
|
||||
end
|
||||
|
||||
desc 'make sure that the current version from the module file matches the last tagged version'
|
||||
@@ -127,28 +54,12 @@ namespace :git do
|
||||
# I need to be able to return this as a data structure
|
||||
# when I start to do more complicated things like
|
||||
# automated releases, I will need this data
|
||||
each_repo do |module_name|
|
||||
require 'puppet'
|
||||
if ! args.project_name || args.project_name == module_name
|
||||
modulefile = File.join(Dir.getwd, 'Modulefile')
|
||||
if File.exists?(modulefile)
|
||||
print module_name
|
||||
metadata = ::Puppet::ModuleTool::Metadata.new
|
||||
::Puppet::ModuleTool::ModulefileReader.evaluate(metadata, modulefile)
|
||||
print ':' + metadata.version
|
||||
branch_output = git_cmd('branch')
|
||||
if branch_output.first =~ /\* (.+)/
|
||||
puts ":#{$1}"
|
||||
puts ' ' + git_cmd("log #{metadata.version}..HEAD --oneline").join("\n ")
|
||||
puts ''
|
||||
else
|
||||
puts ' ' + branch_output.join("\n ")
|
||||
end
|
||||
else
|
||||
puts "#{module_name} does not have a Modulefile"
|
||||
end
|
||||
end
|
||||
check_tags(args.project_name)
|
||||
end
|
||||
|
||||
desc 'make sure that the current version from the module file matches the last tagged version'
|
||||
task :check_all_tags do
|
||||
check_tags
|
||||
end
|
||||
|
||||
task :check_sha_all do
|
||||
@@ -171,100 +82,22 @@ namespace :git do
|
||||
end
|
||||
end
|
||||
|
||||
# list of users that can approve PRs that should run through the integration
|
||||
# tests
|
||||
admin_users = ['bodepd']
|
||||
test_with_this_body = 'test_it'
|
||||
|
||||
namespace :github do
|
||||
|
||||
desc 'pick a single pull request to test. Accepts the project name and number of PR to test'
|
||||
# you can also specify the OPERATINGSYSTEM to test as an ENV variable
|
||||
task :test_pull_request, [:project_name, :number] do |t, args|
|
||||
# TODO - this is way too much overhead, I am reusing each_repo,
|
||||
# but I should write some kind of repo select
|
||||
each_repo do |repo_name|
|
||||
#require 'ruby-debug';debugger
|
||||
if repo_name == args.project_name
|
||||
require 'curb'
|
||||
require 'json'
|
||||
project_url = "https://api.github.com/repos/puppetlabs/puppetlabs-#{args.project_name}"
|
||||
pull_request_url = "#{project_url}/pulls/#{args.number}"
|
||||
resp = Curl.get(pull_request_url)
|
||||
pr = JSON.parse(resp.body_str)
|
||||
|
||||
if ! pr['merged']
|
||||
if pr['mergeable']
|
||||
if pr['comments'] > 0
|
||||
resp = Curl.get("#{project_url}/issues/#{args.number}/comments")
|
||||
comments = JSON.parse(resp.body_str)
|
||||
puts 'going through comments'
|
||||
comments.each do |comment|
|
||||
if admin_users.include?(comment['user']['login'])
|
||||
test_file = File.join(base_dir, '.current_tests')
|
||||
if File.exists?(test_file)
|
||||
loaded_pr = YAML.load_file(test_file)
|
||||
puts "Branch already checkout out for testing #{loaded_pr[:project]}/#{loaded_pr[:number]}"
|
||||
end
|
||||
if comment['body'] == 'test_it'
|
||||
clone_url = pr['head']['repo']['clone_url']
|
||||
remote_name = pr['head']['user']['login']
|
||||
sha = pr['head']['sha']
|
||||
File.open(test_file, 'w') do |fh|
|
||||
fh.write({
|
||||
:project => args.project_name,
|
||||
:number => args.number
|
||||
}.to_yaml)
|
||||
end
|
||||
puts 'found one that we should test'
|
||||
# TODO I am not sure how reliable all of this is going
|
||||
# to be
|
||||
remotes = git_cmd('remote')
|
||||
unless remotes.include?(remote_name)
|
||||
git_cmd("remote add #{remote_name} #{clone_url}")
|
||||
end
|
||||
git_cmd("fetch #{remote_name}")
|
||||
# TODO does that work if master has been updated?
|
||||
git_cmd("checkout #{sha}")
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
puts "PR: #{args.number} from #{args.project_name} has no commits.\
|
||||
I will not test it. We only test things approved.
|
||||
"
|
||||
end
|
||||
else
|
||||
puts "PR: #{args.number} from #{args.project_name} cannot be merged, will not test"
|
||||
end
|
||||
else
|
||||
puts "PR: #{args.number} from #{args.project_name} was already merged, will not test"
|
||||
end
|
||||
end
|
||||
end
|
||||
#GET /repos/:owner/:repo/pulls/:number/comments
|
||||
checkout_pr(args.project_name, args.number, ['bodepd'], 'test_it')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def each_testable_pull_request(&block)
|
||||
end
|
||||
|
||||
namespace :test do
|
||||
|
||||
desc 'test openstack with basic test script on redhat and ubuntu'
|
||||
task 'two_node' do
|
||||
require 'yaml'
|
||||
#Rake::Task['openstack:setup'.to_sym].invoke
|
||||
['redhat', 'ubuntu'].each do |os|
|
||||
cfg = File.join(base_dir, 'config.yaml')
|
||||
yml = YAML.load_file(cfg).merge({'operatingsystem' => os})
|
||||
File.open(cfg, 'w') {|f| f.write(yml.to_yaml) }
|
||||
cmd_system('vagrant destroy -f')
|
||||
deploy_two_node
|
||||
# I should check this to see if the last line is cirros
|
||||
on_box('openstack_controller', 'sudo bash /tmp/test_nova.sh;exit $?')
|
||||
end
|
||||
test_two_node(['redhat', 'ubunut'])
|
||||
end
|
||||
|
||||
desc 'test all in one deployment on redhat/ubuntu (not yet implemented)'
|
||||
@@ -272,60 +105,8 @@ namespace :test do
|
||||
|
||||
end
|
||||
|
||||
task :test do
|
||||
desc 'test that openstack can boot an image from the vagrant bog'
|
||||
task :controller_test do
|
||||
on_box('openstack_controller', 'sudo bash /tmp/foo.sh')
|
||||
end
|
||||
end
|
||||
|
||||
def contributor_hash
|
||||
repos_i_care_about = ['nova', 'glance', 'openstack', 'keystone', 'swift', 'horizon', 'cinder']
|
||||
contributors = {}
|
||||
each_repo do |module_name|
|
||||
if repos_i_care_about.include?(module_name)
|
||||
logs = git_cmd('log --format=short')
|
||||
user_lines = logs.select {|x| x =~ /^Author:\s+(.*)$/ }
|
||||
user_lines.collect do |x|
|
||||
if x =~ /^Author:\s+(.*)?\s+<((\S+)@(\S+))>$/
|
||||
unless ['root', 'vagrant', 'Dan'].include?($1)
|
||||
if contributors[$1]
|
||||
contributors[$1][:repos] = contributors[$1][:repos] | [module_name]
|
||||
else
|
||||
contributors[$1] = {:email => $2, :repos => [module_name] }
|
||||
end
|
||||
else
|
||||
# trimming out extra users
|
||||
end
|
||||
else
|
||||
puts "Skipping unexpected line #{x}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
contributors
|
||||
end
|
||||
|
||||
|
||||
def each_repo(&block)
|
||||
require 'librarian/puppet'
|
||||
require 'librarian/puppet/source/git'
|
||||
# create a manifest
|
||||
# TODO replace this to use librarian puppet
|
||||
env = Librarian::Puppet::Environment.new()
|
||||
# this is the lock file, so it assumes that install has been run
|
||||
env.lock.manifests.each do |manifest|
|
||||
# I only care about git sources
|
||||
if manifest.source.is_a? Librarian::Puppet::Source::Git
|
||||
module_name = manifest.name.split('/', 2)[1]
|
||||
module_path = File.join(env.install_path,module_name)
|
||||
if File.directory?(module_path)
|
||||
Dir.chdir(module_path) do
|
||||
yield module_name
|
||||
end
|
||||
else
|
||||
puts "Module directory #{module_path} does not exist... How strange."
|
||||
end
|
||||
else
|
||||
puts "Found a non-git manifest: #{manifest.class}, ignoring"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
286
lib/puppetlabs/os_tester.rb
Normal file
286
lib/puppetlabs/os_tester.rb
Normal file
@@ -0,0 +1,286 @@
|
||||
#
|
||||
# class that hold utilities that I use to test openstack
|
||||
#
|
||||
module Puppetlabs
|
||||
module OsTester
|
||||
|
||||
def cmd_system (cmd)
|
||||
result = system cmd
|
||||
raise(RuntimeError, $?) unless $?.success?
|
||||
result
|
||||
end
|
||||
|
||||
def git_cmd(cmd)
|
||||
command = 'git ' + cmd
|
||||
Open3.popen3(*command) do |i, o, e, t|
|
||||
raise StandardError, e.read unless (t ? t.value : $?).success?
|
||||
o.read.split("\n")
|
||||
end
|
||||
end
|
||||
|
||||
def on_box (box, cmd)
|
||||
cmd_system("vagrant ssh #{box} -c '#{cmd}'")
|
||||
end
|
||||
|
||||
def deploy_two_node
|
||||
require 'vagrant'
|
||||
env = Vagrant::Environment.new(:cwd => base_dir, :ui_class => Vagrant::UI::Colored)
|
||||
build(:openstack_controller, env)
|
||||
build(:compute1, env)
|
||||
end
|
||||
|
||||
# bring vagrant vm with image name up
|
||||
def build(instance, env)
|
||||
unless vm = env.vms[instance]
|
||||
puts "invalid VM: #{instance}"
|
||||
else
|
||||
if vm.created?
|
||||
puts "VM: #{instance} was already created"
|
||||
else
|
||||
# be very fault tolerant :)
|
||||
begin
|
||||
# this will always fail
|
||||
vm.up(:provision => true)
|
||||
rescue Exception => e
|
||||
puts e.class
|
||||
puts e
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def each_repo(&block)
|
||||
require 'librarian/puppet'
|
||||
require 'librarian/puppet/source/git'
|
||||
# create a manifest
|
||||
# TODO replace this to use librarian puppet
|
||||
env = Librarian::Puppet::Environment.new()
|
||||
# this is the lock file, so it assumes that install has been run
|
||||
env.lock.manifests.each do |manifest|
|
||||
# I only care about git sources
|
||||
if manifest.source.is_a? Librarian::Puppet::Source::Git
|
||||
module_name = manifest.name.split('/', 2)[1]
|
||||
module_path = File.join(env.install_path,module_name)
|
||||
if File.directory?(module_path)
|
||||
Dir.chdir(module_path) do
|
||||
yield module_name
|
||||
end
|
||||
else
|
||||
puts "Module directory #{module_path} does not exist... How strange."
|
||||
end
|
||||
else
|
||||
puts "Found a non-git manifest: #{manifest.class}, ignoring"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def contributor_hash(
|
||||
repos_i_care_about = ['nova', 'glance', 'openstack', 'keystone', 'swift', 'horizon', 'cinder']
|
||||
)
|
||||
contributors = {}
|
||||
each_repo do |module_name|
|
||||
if repos_i_care_about.include?(module_name)
|
||||
logs = git_cmd('log --format=short')
|
||||
user_lines = logs.select {|x| x =~ /^Author:\s+(.*)$/ }
|
||||
user_lines.collect do |x|
|
||||
if x =~ /^Author:\s+(.*)?\s+<((\S+)@(\S+))>$/
|
||||
unless ['root', 'vagrant', 'Dan'].include?($1)
|
||||
if contributors[$1]
|
||||
contributors[$1][:repos] = contributors[$1][:repos] | [module_name]
|
||||
else
|
||||
contributors[$1] = {:email => $2, :repos => [module_name] }
|
||||
end
|
||||
else
|
||||
# trimming out extra users
|
||||
end
|
||||
else
|
||||
puts "Skipping unexpected line #{x}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
contributors
|
||||
end
|
||||
|
||||
|
||||
# destroy all vagrant instances
|
||||
def destroy_all_vms
|
||||
puts "About to destroy all vms..."
|
||||
cmd_system('vagrant destroy -f')
|
||||
puts "Destroyed all vms"
|
||||
end
|
||||
|
||||
|
||||
# adds the specified remote name as a read/write remote
|
||||
def dev_setup(remote_name)
|
||||
each_repo do |module_name|
|
||||
# need to handle more failure cases
|
||||
remotes = git_cmd('remote')
|
||||
if remotes.include?(remote_name)
|
||||
puts "Did not have to add remote #{remote_name} to #{module_name}"
|
||||
elsif ! remotes.include?('origin')
|
||||
raise(Exception, "Repo #{module_name} has no remote called origin, failing")
|
||||
else
|
||||
remote_url = git_cmd('remote show origin').detect {|x| x =~ /\s+Push\s+URL: / }
|
||||
if remote_url =~ /(git|https?):\/\/(.+)\/(.+)?\/(.+)/
|
||||
url = "git@#{$2}:#{remote_name}/#{$4}"
|
||||
else
|
||||
puts "remote_url #{remote_url} did not have the expected format. weird..."
|
||||
end
|
||||
puts "Adding remote #{remote_name} as #{url}"
|
||||
git_cmd("remote add #{remote_name} #{url}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def pull_all
|
||||
each_repo do |module_name|
|
||||
puts "Pulling repo: #{module_name}"
|
||||
puts ' ' + git_cmd('pull').join("\n ")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def status_all
|
||||
each_repo do |module_name|
|
||||
status = git_cmd('status')
|
||||
if status.include?('nothing to commit (working directory clean)')
|
||||
puts "Module #{module_name} has not changed" if verbose
|
||||
else
|
||||
puts "Uncommitted changes for: #{module_name}"
|
||||
puts " #{status.join("\n ")}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_tags(project_name=nil)
|
||||
each_repo do |module_name|
|
||||
require 'puppet'
|
||||
if ! project_name || project_name == module_name
|
||||
modulefile = File.join(Dir.getwd, 'Modulefile')
|
||||
if File.exists?(modulefile)
|
||||
print module_name
|
||||
metadata = ::Puppet::ModuleTool::Metadata.new
|
||||
::Puppet::ModuleTool::ModulefileReader.evaluate(metadata, modulefile)
|
||||
print ':' + metadata.version
|
||||
branch_output = git_cmd('branch')
|
||||
if branch_output.first =~ /\* (.+)/
|
||||
puts ":#{$1}"
|
||||
puts ' ' + git_cmd("log #{metadata.version}..HEAD --oneline").join("\n ")
|
||||
puts ''
|
||||
else
|
||||
puts ' ' + branch_output.join("\n ")
|
||||
end
|
||||
else
|
||||
puts "#{module_name} does not have a Modulefile"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# given a pull request, return true if we should test it.
|
||||
# this means that is can be merged, and has a comment where one of the admin users
|
||||
# has specified the expected body.
|
||||
def testable_pull_request?(
|
||||
pr,
|
||||
admin_users,
|
||||
project_base_url = 'https://api.github.com/repos/puppetlabs/',
|
||||
expected_body = 'test_it'
|
||||
)
|
||||
project_url = project_base_url + pr['base']['repo']['name']
|
||||
if ! pr['merged']
|
||||
if pr['mergeable']
|
||||
if pr['comments'] > 0
|
||||
resp = Curl.get("#{project_url}/issues/#{pr['number']}/comments")
|
||||
comments = JSON.parse(resp.body_str)
|
||||
puts 'going through comments'
|
||||
comments.each do |comment|
|
||||
if admin_users.include?(comment['user']['login'])
|
||||
if comment['body'] == expected_body
|
||||
return true
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
else
|
||||
puts "PR: #{pr['number']} from #{project_name} has no commits.\
|
||||
I will not test it. We only test things approved.
|
||||
"
|
||||
end
|
||||
else
|
||||
puts "PR: #{pr['number']} from #{project_name} cannot be merged, will not test"
|
||||
end
|
||||
else
|
||||
puts "PR: #{pr['number']} from #{project_name} was already merged, will not test"
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def checkout_pr(project_name, number, admin_users, expected_body)
|
||||
# but I should write some kind of repo select
|
||||
# depends on https://github.com/peter-murach/github
|
||||
require 'github_api'
|
||||
require 'curb'
|
||||
require 'json'
|
||||
|
||||
each_repo do |repo_name|
|
||||
if repo_name == project_name
|
||||
project_url = "https://api.github.com/repos/puppetlabs/puppetlabs-#{project_name}"
|
||||
pull_request_url = "#{project_url}/pulls/#{number}"
|
||||
resp = Curl.get(pull_request_url)
|
||||
pr = JSON.parse(resp.body_str)
|
||||
# need to be able to override this?
|
||||
test_file = File.join(base_dir, '.current_testing')
|
||||
if File.exists?(test_file)
|
||||
loaded_pr = YAML.load_file(test_file)
|
||||
puts "Branch already checked out for testing #{loaded_pr[:project]}/#{loaded_pr[:number]}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
if testable_pull_request?(pr, admin_users)
|
||||
clone_url = pr['head']['repo']['clone_url']
|
||||
remote_name = pr['head']['user']['login']
|
||||
sha = pr['head']['sha']
|
||||
File.open(test_file, 'w') do |fh|
|
||||
fh.write({
|
||||
:project => project_name,
|
||||
:number => number
|
||||
}.to_yaml)
|
||||
end
|
||||
puts 'found one that we should test'
|
||||
# TODO I am not sure how reliable all of this is going
|
||||
# to be
|
||||
remotes = git_cmd('remote')
|
||||
unless remotes.include?(remote_name)
|
||||
git_cmd("remote add #{remote_name} #{clone_url}")
|
||||
end
|
||||
git_cmd("fetch #{remote_name}")
|
||||
# TODO does that work if master has been updated?
|
||||
git_cmd("checkout #{sha}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_two_node(oses = [])
|
||||
require 'yaml'
|
||||
#Rake::Task['openstack:setup'.to_sym].invoke
|
||||
oses.each do |os|
|
||||
cfg = File.join(base_dir, 'config.yaml')
|
||||
yml = YAML.load_file(cfg).merge({'operatingsystem' => os})
|
||||
File.open(cfg, 'w') {|f| f.write(yml.to_yaml) }
|
||||
cmd_system('vagrant destroy -f')
|
||||
deploy_two_node
|
||||
# I should check this to see if the last line is cirros
|
||||
on_box('openstack_controller', 'sudo bash /tmp/test_nova.sh;exit $?')
|
||||
end
|
||||
end
|
||||
|
||||
# iterate through each testable pull request
|
||||
def each_testable_pull_request(&block)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user