Commit 3ff00693 authored by Andrey Filippov's avatar Andrey Filippov

implemented transfer over ssh (10x slower), restored input(), forced python3

parent 5e1a0164
#!/usr/bin/env python #!/usr/bin/env python3
# Download footage from ssd's raw partition: # Download footage from ssd's raw partition:
# * standalone SSDs connected using a docking station or enclosure or else, raw partition, camera can be powered off # * standalone SSDs connected using a docking station or enclosure or else, raw partition, camera can be powered off
...@@ -72,6 +72,7 @@ parser.add_argument("-bc",type=int,default=512,help="Number of blocks of size [b ...@@ -72,6 +72,7 @@ parser.add_argument("-bc",type=int,default=512,help="Number of blocks of size [b
parser.add_argument("-fs","--file_start",default="",help=".disk file name with start LBA pointer, will calculate 'skip' value") parser.add_argument("-fs","--file_start",default="",help=".disk file name with start LBA pointer, will calculate 'skip' value")
parser.add_argument("-fe","--file_end",default="camogm.disk",help=".disk file name with end LBA pointer, default is 'camogm.disk'") parser.add_argument("-fe","--file_end",default="camogm.disk",help=".disk file name with end LBA pointer, default is 'camogm.disk'")
parser.add_argument("dest",help="desitnation directory: /data/footage/test") parser.add_argument("dest",help="desitnation directory: /data/footage/test")
parser.add_argument("-l","--lan", action='store_true', help="Transfer data over LAN instead of eSATA")
args = parser.parse_args() args = parser.parse_args()
print (args) print (args)
...@@ -106,47 +107,69 @@ if len(cams)==0: ...@@ -106,47 +107,69 @@ if len(cams)==0:
pc = x393.PC() pc = x393.PC()
# ssd to camera for all
for cam in cams:
cam['obj'].ssd_to_camera()
# get raw partitions
if (args.lan):
print ("Using data transfer over LAN")
else:
print ("Using data transfer over eSATA, multiplexing camera SSD to eSATA external port")
# ssd to camera for all
for cam in cams:
cam['obj'].ssd_to_camera()
# get raw partitions
for cam in cams: for cam in cams:
d = cam['obj'].first_found_raw_partition_name() d = cam['obj'].first_found_raw_partition_name() # ssh
if d!="undefined": if d!="undefined":
dirs.append(d) dirs.append(d)
print(cam['user']+"@"+cam['ip']+": raw partition name: "+d) print(cam['user']+"@"+cam['ip']+": raw partition name: "+d)
else: else:
cam['disable']=1 cam['disable']=1
print(bcolors.FAIL+cam['user']+"@"+cam['ip']+" : error: already switched or raw partition not found"+bcolors.ENDC) print(bcolors.FAIL+cam['user']+"@"+cam['ip']+" : error: already switched or raw partition not found"+bcolors.ENDC)
#raise Exception(cam['user']+"@"+cam['ip']+" : error: already switched or raw partition not found") #raise Exception(cam['user']+"@"+cam['ip']+" : error: already switched or raw partition not found")
# no need
# no need cams[:] = [tmp for tmp in cams if tmp.get('disable')!=1]
cams[:] = [tmp for tmp in cams if tmp.get('disable')!=1]
if (not args.lan):
# switch ssd to pc for all (not caring which cable is in) # switch ssd to pc for all (not caring which cable is in)
for cam in cams: for cam in cams:
cam['obj'].ssd_to_pc() cam['obj'].ssd_to_pc()
# download # download
plist = [] plist = []
all_downloaded = False all_downloaded = False
for i in range(len(cams)): #for i in range(len(cams)):
raw_input(bcolors.OKGREEN+"Connect camera (eSATA) to PC (eSATA/SATA). Press Enter to continue..."+bcolors.ENDC) for i,cam in enumerate(cams):
# raw_input(bcolors.OKGREEN+"Connect camera (eSATA) to PC (eSATA/SATA). Press Enter to continue..."+bcolors.ENDC)
if (args.lan):
print(bcolors.OKGREEN+"Initiating data transfer over LAN ..."+bcolors.ENDC)
else:
input(bcolors.OKGREEN+"Connect camera (eSATA) to PC (eSATA/SATA). Press Enter to continue..."+bcolors.ENDC)
proceed_to_next = False proceed_to_next = False
t = 0 t = 0
while not all_downloaded: while not all_downloaded:
plist = pc.list_partitions() if (args.lan):
plist = cam['obj'].list_partitions_as_host() # Camera partitions, same format as for host (label, path)
#[['NT-1TB_2242_0007988000104', 'sda'], ['NT-1TB_2242_0007988000104-part1', 'sda1'], ['NT-1TB_2242_0007988000104-part2', 'sda2']]
else:
plist = pc.list_partitions() # HOST(!) partitions, such as /dev/sdc1
#[[u'NT-1TB_2242_0007988000104', u'sdc'], [u'NT-1TB_2242_0007988000104-part1', u'sdc1'], [u'NT-1TB_2242_0007988000104-part2', u'sdc2']]
for d in dirs: for d in dirs:
for p in plist: for p in plist:
if d==p[0]: if d==p[0]:
# p[1] == sdb2 # p[1] == sdb2
# hardcoded /dev/sd?1 # hardcoded /dev/sd?1
if args.n==0: if args.n==0:
data_size_blocks = pc.read_camogm_disk_file_blocks("/dev/"+p[1][0:-1]+"1", args.file_end) if (args.lan):
data_skip_blocks = pc.read_camogm_disk_file_blocks("/dev/"+p[1][0:-1]+"1", args.file_start) # data_size_blocks = pc.read_camogm_disk_file_blocks_lan(cams[i].ip, p[1][0:-1]+"1", args.file_end)
# data_skip_blocks = pc.read_camogm_disk_file_blocks_lan(cams[i].ip, p[1][0:-1]+"1", args.file_start)
data_size_blocks = cam['obj'].read_camogm_disk_file_blocks(p[1][0:-1]+"1", args.file_end)
data_skip_blocks = cam['obj'].read_camogm_disk_file_blocks(p[1][0:-1]+"1", args.file_start)
#read_camogm_disk_file_blocks
else:
data_size_blocks = pc.read_camogm_disk_file_blocks("/dev/"+p[1][0:-1]+"1", args.file_end)
data_skip_blocks = pc.read_camogm_disk_file_blocks("/dev/"+p[1][0:-1]+"1", args.file_start)
data_size_blocks -= data_skip_blocks # before it included skipped ! data_size_blocks -= data_skip_blocks # before it included skipped !
else: else:
data_size_blocks = args.n data_size_blocks = args.n
...@@ -158,34 +181,31 @@ for i in range(len(cams)): ...@@ -158,34 +181,31 @@ for i in range(len(cams)):
block_size= 512 # 4096 block_size= 512 # 4096
data_size_gb = (data_size_blocks * block_size) / (1024 * 1024 * 1024) data_size_gb = (data_size_blocks * block_size) / (1024 * 1024 * 1024)
file_gb = args.bs*args.bc // 1024 # just for compatibility, change to parameter file_gb = args.bs*args.bc // 1024 # just for compatibility, change to parameter
print("Data size: %d %d-byte blocks (%f GB)"%(data_size_blocks, block_size, data_size_gb)) print("Data size: %d %d-byte blocks (%f GB)"%(data_size_blocks, block_size, data_size_gb))
pc.download_blocks(args.dest, # def download_blocks(self, dest, part, blocks_load, blocks_skip= 0, file_gb=10, chunk_blocks=32768, block_size=512): if (args.lan):
"/dev/"+p[1], print(cam)
blocks_load=data_size_blocks, pc.download_blocks_lan(
blocks_skip= data_skip_blocks, cam,
file_gb=file_gb, args.dest,
chunk_blocks=chunk_blocks, "/dev/"+p[1],
block_size=block_size) blocks_load=data_size_blocks,
''' blocks_skip= data_skip_blocks,
data_size = pc.read_camogm_disk_file("/dev/"+p[1][0:-1]+"1") file_gb=file_gb,
data_size = round(data_size,2) chunk_blocks=chunk_blocks,
# bs is in kB block_size=block_size)
chunk_size = float(args.bs*args.bc)/1024 else:
n_chunks = int(math.ceil(data_size/chunk_size)) pc.download_blocks( # after testing use download_blocks_lan (rename/delete download_blocks) with cam=None
args.dest,
if args.n==0: "/dev/"+p[1],
args.n = n_chunks - args.skip blocks_load=data_size_blocks,
blocks_skip= data_skip_blocks,
print("Data size: "+str(data_size)+" GB") file_gb=file_gb,
print(bcolors.BOLDWHITE+"Download size: "+str(args.n)+"x "+str(round(chunk_size,2))+"GB, skipped the first "+str(args.skip)+" chunks"+bcolors.ENDC) chunk_blocks=chunk_blocks,
pc.download (args.dest, "/dev/"+p[1], args.bs,args.bc,args.skip,args.n) block_size=block_size)
'''
# def download(self,dest,part,dl_bs=20,dl_bc=512,dl_skip=0,dl_n=0):
#parser.add_argument("-bs",type=int,default=20,help="block size in MB, default = 20")
#parser.add_argument("-bc",type=int,default=512,help="Number of blocks of size [bs] in a single chunk, default = 512, so the default chunk size is 10GB")
dirs.remove(d) dirs.remove(d)
proceed_to_next = True proceed_to_next = True
if len(dirs)!=0: if len(dirs) != 0:
print("wait for the next ssd") print("wait for the next ssd")
else: else:
all_downloaded = True all_downloaded = True
......
...@@ -11,6 +11,7 @@ import re ...@@ -11,6 +11,7 @@ import re
import subprocess import subprocess
import time import time
import tempfile import tempfile
import shutil
def shout(cmd): def shout(cmd):
#subprocess.call prints to console #subprocess.call prints to console
...@@ -39,6 +40,7 @@ class Camera: ...@@ -39,6 +40,7 @@ class Camera:
self.user = user self.user = user
self.ip = ip self.ip = ip
self.sshcmd = "ssh "+user+"@"+ip self.sshcmd = "ssh "+user+"@"+ip
self.scpcmd = "scp "+user+"@"+ip+":"
self.disable = False self.disable = False
self.pattern = "ata-" self.pattern = "ata-"
self.check_connection() self.check_connection()
...@@ -83,6 +85,18 @@ class Camera: ...@@ -83,6 +85,18 @@ class Camera:
plist.append(s0.group(0)) plist.append(s0.group(0))
return plist return plist
def list_partitions_as_host(self):
res = shout(self.sshcmd+" 'ls /dev/disk/by-id/ -all ' | grep '"+self.pattern+"'")
plist = []
for line in res.splitlines():
items = line.split(" ")
for name in items:
if name[0:len(self.pattern)]==self.pattern:
item = items[-1].split("/")
plist.append([name[len(self.pattern):],item[-1]])
return plist
def list_mounted_partitions(self): def list_mounted_partitions(self):
res = shout(self.sshcmd+" 'df -h'") res = shout(self.sshcmd+" 'df -h'")
df_list = [] df_list = []
...@@ -156,6 +170,26 @@ class Camera: ...@@ -156,6 +170,26 @@ class Camera:
time.sleep(1) time.sleep(1)
shout(self.sshcmd+" 'echo 1 > /sys/devices/soc0/amba@0/80000000.elphel-ahci/load_module'") shout(self.sshcmd+" 'echo 1 > /sys/devices/soc0/amba@0/80000000.elphel-ahci/load_module'")
def read_camogm_disk_file_blocks(self, part,fname="camogm.disk"):
result = 0
tmp_mount_point = tempfile.mkdtemp()
print(self.scpcmd+"/mnt/"+part+"/"+fname+" "+tmp_mount_point)
shout(self.scpcmd+"/mnt/"+part+"/"+fname+" "+tmp_mount_point)
try:
with open (tmp_mount_point+"/"+fname, "r") as myfile:
data=myfile.readlines()
if len(data)==2:
l2 = data[1]
pointers = l2.split("\t")
pntr1 = int(pointers[1])
pntr2 = int(pointers[2])
result = pntr2-pntr1
except IOError:
print(tmp_mount_point+"/"+fname+" NOT FOUND")
# os.rmdir(tmp_mount_point)
shutil.rmtree(tmp_mount_point, ignore_errors=True) # non-empty, may contain read-only
return result
class PC(): class PC():
def __init__(self): def __init__(self):
self.pattern = "ata-" self.pattern = "ata-"
...@@ -174,9 +208,7 @@ class PC(): ...@@ -174,9 +208,7 @@ class PC():
# mounts partition (/dev/sd?1), reads camogm.disk file # mounts partition (/dev/sd?1), reads camogm.disk file
# returns the download size from raw partition ((/dev/sd?2)) # returns the download size from raw partition ((/dev/sd?2))
def read_camogm_disk_file(self,part,fname="camogm.disk"): def read_camogm_disk_file(self,part,fname="camogm.disk"):
result = 0 result = 0
tmp_mount_point = tempfile.mkdtemp() tmp_mount_point = tempfile.mkdtemp()
print("mounting "+part+" to "+tmp_mount_point) print("mounting "+part+" to "+tmp_mount_point)
shout("sudo mount "+part+" "+tmp_mount_point) shout("sudo mount "+part+" "+tmp_mount_point)
...@@ -190,15 +222,34 @@ class PC(): ...@@ -190,15 +222,34 @@ class PC():
pntr1 = int(pointers[1]) pntr1 = int(pointers[1])
pntr2 = int(pointers[2]) pntr2 = int(pointers[2])
result = float(pntr2-pntr1)*512/1024/1024/1024 # still 512 block size result = float(pntr2-pntr1)*512/1024/1024/1024 # still 512 block size
# result = float(pntr2-pntr1)*4096/1024/1024/1024 # blocks are now 4096, not 512 bytes!
except IOError: except IOError:
print(tmp_mount_point+"/"+fname+" NOT FOUND") print(tmp_mount_point+"/"+fname+" NOT FOUND")
shout("sudo umount "+tmp_mount_point) shout("sudo umount "+tmp_mount_point)
os.rmdir(tmp_mount_point) os.rmdir(tmp_mount_point)
return result
#similar as read_camogm_disk_file(), but uses LAN istead of eSATA. Needs additional argument ip
def read_camogm_disk_file_lan(self, ip, part, fname="camogm.disk"):
result = 0
tmp_mount_point = tempfile.mkdtemp()
print("scp root@"+ip+":/mnt/"+part+"/"+fname+" "+tmp_mount_point)
shout("scp root@"+ip+":/mnt/"+part+"/"+fname+" "+tmp_mount_point)
try:
with open (tmp_mount_point+"/"+fname, "r") as myfile:
data=myfile.readlines()
if len(data)==2:
l2 = data[1]
pointers = l2.split("\t")
pntr1 = int(pointers[1])
pntr2 = int(pointers[2])
result = float(pntr2-pntr1)*512/1024/1024/1024 # still 512 block size
except IOError:
print(tmp_mount_point+"/"+fname+" NOT FOUND")
# os.rmdir(tmp_mount_point)
shutil.rmtree(tmp_mount_point, ignore_errors=True) # non-empty, may contain read-only
return result return result
""" """
Returns block offset from the start of partition to the current LBA pointer Returns block offset from the start of partition to the current LBA pointer
""" """
...@@ -207,7 +258,6 @@ class PC(): ...@@ -207,7 +258,6 @@ class PC():
tmp_mount_point = tempfile.mkdtemp() tmp_mount_point = tempfile.mkdtemp()
print("mounting "+part+" to "+tmp_mount_point) print("mounting "+part+" to "+tmp_mount_point)
shout("sudo mount "+part+" "+tmp_mount_point) shout("sudo mount "+part+" "+tmp_mount_point)
try: try:
with open (tmp_mount_point+"/"+fname, "r") as myfile: with open (tmp_mount_point+"/"+fname, "r") as myfile:
data=myfile.readlines() data=myfile.readlines()
...@@ -221,12 +271,30 @@ class PC(): ...@@ -221,12 +271,30 @@ class PC():
result = pntr2-pntr1 result = pntr2-pntr1
except IOError: except IOError:
print(tmp_mount_point+"/"+fname+" NOT FOUND") print(tmp_mount_point+"/"+fname+" NOT FOUND")
shout("sudo umount "+tmp_mount_point) shout("sudo umount "+tmp_mount_point)
os.rmdir(tmp_mount_point) os.rmdir(tmp_mount_point)
return result return result
def read_camogm_disk_file_blocks_lan(self, ip, part,fname="camogm.disk"):
result = 0
tmp_mount_point = tempfile.mkdtemp()
print("scp root@"+ip+":/mnt/"+part+"/"+fname+" "+tmp_mount_point)
shout("scp root@"+ip+":/mnt/"+part+"/"+fname+" "+tmp_mount_point)
try:
with open (tmp_mount_point+"/"+fname, "r") as myfile:
data=myfile.readlines()
if len(data)==2:
l2 = data[1]
pointers = l2.split("\t")
pntr1 = int(pointers[1])
pntr2 = int(pointers[2])
# result = float(pntr2-pntr1)*512/1024/1024/1024
result = pntr2-pntr1
except IOError:
print(tmp_mount_point+"/"+fname+" NOT FOUND")
# os.rmdir(tmp_mount_point)
shutil.rmtree(tmp_mount_point, ignore_errors=True) # non-empty, may contain read-only
return result
def is_raw(self,part): def is_raw(self,part):
res = shout("sudo blkid | grep "+str(part)) res = shout("sudo blkid | grep "+str(part))
...@@ -266,7 +334,6 @@ class PC(): ...@@ -266,7 +334,6 @@ class PC():
if i>=dl_skip: if i>=dl_skip:
shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(dl_bs)+"M count="+str(dl_bc)+" skip="+str(skip)) shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(dl_bs)+"M count="+str(dl_bc)+" skip="+str(skip))
# def download_blocks(self, dest, part, blocks_load, blocks_skip= 0, file_gb=10, chunk_blocks=4096, block_size=4096):
def download_blocks(self, dest, part, blocks_load, blocks_skip= 0, file_gb=10, chunk_blocks=32768, block_size=512): #4096): def download_blocks(self, dest, part, blocks_load, blocks_skip= 0, file_gb=10, chunk_blocks=32768, block_size=512): #4096):
chunk_bytes = block_size * chunk_blocks chunk_bytes = block_size * chunk_blocks
file_chunks = (file_gb * 1024 * 1024 * 1024) // chunk_bytes file_chunks = (file_gb * 1024 * 1024 * 1024) // chunk_bytes
...@@ -313,8 +380,75 @@ class PC(): ...@@ -313,8 +380,75 @@ class PC():
fname = "%s/file_%03d.img" %(dirname, num_file) #dirname+"/"+"file_"+str(num_file)+".img" fname = "%s/file_%03d.img" %(dirname, num_file) #dirname+"/"+"file_"+str(num_file)+".img"
print("Downloading last %d %d-byte blocks, skipping %d blocks to %s"%(blocks_load, block_size, blocks_skip, fname)) print("Downloading last %d %d-byte blocks, skipping %d blocks to %s"%(blocks_load, block_size, blocks_skip, fname))
shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(block_size)+" count="+str(blocks_load)+" skip="+str(blocks_skip)) shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(block_size)+" count="+str(blocks_load)+" skip="+str(blocks_skip))
#time ssh root@192.168.0.41 "dd if=/dev/sda2 bs=16777216 count=409 skip=322" | dd of=/home/elphel/lwir16-proc/test_dd/file_0001.img
# res = shout(self.sshcmd+" 'ls -all /dev/disk/by-id | grep '"+self.pattern+"' | grep '"+partition[-4:]+"''")
def download_blocks_lan(self, cam, dest, part, blocks_load, blocks_skip= 0, file_gb=10, chunk_blocks=32768, block_size=512): #4096):
chunk_bytes = block_size * chunk_blocks
file_chunks = (file_gb * 1024 * 1024 * 1024) // chunk_bytes
if (cam==None):
self.is_raw(part)
ip = None
else:
print("TODO: Implement raw partition check over SSH")
# print(cam)
ip = cam["ip"]
print("Getting raw partition data from "+part)
if not os.path.isdir(dest):
os.mkdir(dest)
dirname = self.partname(part)
if dirname!="":
dirname = dest+"/"+dirname
if not os.path.isdir(dirname):
os.mkdir(dirname)
num_file = 0
# optional first file to align skip to chunk_blocks, 1 block at a time
if (blocks_skip > 0) and ((blocks_skip % chunk_blocks) > 0):
bwrite = chunk_blocks - (blocks_skip % chunk_blocks)
if (bwrite > blocks_load):
bwrite = blocks_load
fname = "%s/file_%03d.img" %(dirname, num_file) #dirname+"/"+"file_"+str(num_file)+".img"
print("Aligning skip to chunks, downloading %d %d-byte blocks (skipping %d blocks) to %s"%(bwrite, block_size, blocks_skip, fname))
if (cam == None):
print("sudo dd if="+part+" "+" of="+fname+" bs="+str(block_size)+" count="+str(bwrite)+" skip="+str(blocks_skip))
shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(block_size)+" count="+str(bwrite)+" skip="+str(blocks_skip))
else:
print(cam['obj'].sshcmd+" 'dd if="+part+" bs="+str(block_size)+" count="+str(bwrite)+" skip="+str(blocks_skip)+"' | dd of="+fname)
shout(cam['obj'].sshcmd+" 'dd if="+part+" bs="+str(block_size)+" count="+str(bwrite)+" skip="+str(blocks_skip)+"' | dd of="+fname)
blocks_skip += bwrite
blocks_load -= bwrite
num_file += 1
# write bulk of the blocks, <= file_chunks of chunks in each file
while ((blocks_load // chunk_blocks) > 0):
chunks_write = blocks_load // chunk_blocks
chunks_skip = blocks_skip // chunk_blocks # should be already multiple of chunks
if (chunks_write > file_chunks):
chunks_write = file_chunks
# fname = dirname+"/"+"file_"+str(num_file)+".img"
fname = "%s/file_%03d.img" %(dirname, num_file) #dirname+"/"+"file_"+str(num_file)+".img"
print("Downloading %d %d-byte chunks, skipping %d chunks to %s"%(chunks_write, chunk_bytes, chunks_skip, fname))
if (ip == None):
print("sudo dd if="+part+" "+" of="+fname+" bs="+str(chunk_bytes)+" count="+str(chunks_write)+" skip="+str(chunks_skip))
shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(chunk_bytes)+" count="+str(chunks_write)+" skip="+str(chunks_skip))
else:
print(cam['obj'].sshcmd+" 'dd if="+part+" bs="+str(chunk_bytes)+" count="+str(chunks_write)+" skip="+str(chunks_skip)+"' | dd of="+fname)
shout(cam['obj'].sshcmd+" 'dd if="+part+" bs="+str(chunk_bytes)+" count="+str(chunks_write)+" skip="+str(chunks_skip)+"' | dd of="+fname)
bwrite = chunks_write * chunk_blocks
blocks_skip += bwrite
blocks_load -= bwrite
num_file += 1
# optionally write the remainder (< chunk), 1 block at a time
if (blocks_load > 0):
# fname = dirname+"/"+"file_"+str(num_file)+".img"
fname = "%s/file_%03d.img" %(dirname, num_file) #dirname+"/"+"file_"+str(num_file)+".img"
print("Downloading last %d %d-byte blocks, skipping %d blocks to %s"%(blocks_load, block_size, blocks_skip, fname))
if (ip == None):
print("sudo dd if="+part+" "+" of="+fname+" bs="+str(block_size)+" count="+str(blocks_load)+" skip="+str(blocks_skip))
shout("sudo dd if="+part+" "+" of="+fname+" bs="+str(block_size)+" count="+str(blocks_load)+" skip="+str(blocks_skip))
else:
print(cam['obj'].sshcmd+" 'dd if="+part+" bs="+str(block_size)+" count="+str(blocks_load)+" skip="+str(blocks_skip)+"' | dd of="+fname)
shout(cam['obj'].sshcmd+" 'dd if="+part+" bs="+str(block_size)+" count="+str(blocks_load)+" skip="+str(blocks_skip)+"' | dd of="+fname)
def partname(self,partition): def partname(self,partition):
cmd = "ls /dev/disk/by-id/ -all | grep '"+self.pattern+"' | grep '"+partition[-4:]+"'" cmd = "ls /dev/disk/by-id/ -all | grep '"+self.pattern+"' | grep '"+partition[-4:]+"'"
res = shout(cmd) res = shout(cmd)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment