From f5b1d6dd7ade04eddd4ce85856efb30ef1072466 Mon Sep 17 00:00:00 2001 From: Britney Fransen Date: Mon, 21 Nov 2022 01:04:00 -0500 Subject: linhes-system: initial inclusion --- linhes/linhes-system/10-monitor.conf | 3 + linhes/linhes-system/79-cronie.hook | 9 + linhes/linhes-system/PKGBUILD | 87 ++ linhes/linhes-system/add_storage.py | 1185 ++++++++++++++++++++++++ linhes/linhes-system/add_storage.readme | 3 + linhes/linhes-system/balance_storage_groups.py | 153 +++ linhes/linhes-system/be_check.py | 35 + linhes/linhes-system/checkXFSfrag.sh | 70 ++ linhes/linhes-system/create_media_dirs.sh | 74 ++ linhes/linhes-system/empty_storage_groups.py | 195 ++++ linhes/linhes-system/enableIRWake.sh | 22 + linhes/linhes-system/find_orphans.py | 249 +++++ linhes/linhes-system/fstrim.hook | 9 + linhes/linhes-system/idle.py | 375 ++++++++ linhes/linhes-system/jobqueue_helper.py | 63 ++ linhes/linhes-system/lh_systemstart.sh | 29 + linhes/linhes-system/lh_systemstart.sh.desktop | 7 + linhes/linhes-system/myth2mkv | 466 ++++++++++ linhes/linhes-system/myth2mp3 | 96 ++ linhes/linhes-system/myth_mtc.cron | 79 ++ linhes/linhes-system/openssh.hook | 9 + linhes/linhes-system/paccache.cron | 2 + linhes/linhes-system/readme_is_xml | 1 + linhes/linhes-system/remove_storage.py | 184 ++++ linhes/linhes-system/system-sudo.rules | 1 + linhes/linhes-system/xfs_defrag.cron | 3 + 26 files changed, 3409 insertions(+) create mode 100644 linhes/linhes-system/10-monitor.conf create mode 100644 linhes/linhes-system/79-cronie.hook create mode 100755 linhes/linhes-system/PKGBUILD create mode 100755 linhes/linhes-system/add_storage.py create mode 100644 linhes/linhes-system/add_storage.readme create mode 100755 linhes/linhes-system/balance_storage_groups.py create mode 100755 linhes/linhes-system/be_check.py create mode 100755 linhes/linhes-system/checkXFSfrag.sh create mode 100755 linhes/linhes-system/create_media_dirs.sh create mode 100755 linhes/linhes-system/empty_storage_groups.py create mode 100755 linhes/linhes-system/enableIRWake.sh create mode 100755 linhes/linhes-system/find_orphans.py create mode 100644 linhes/linhes-system/fstrim.hook create mode 100755 linhes/linhes-system/idle.py create mode 100755 linhes/linhes-system/jobqueue_helper.py create mode 100755 linhes/linhes-system/lh_systemstart.sh create mode 100644 linhes/linhes-system/lh_systemstart.sh.desktop create mode 100755 linhes/linhes-system/myth2mkv create mode 100755 linhes/linhes-system/myth2mp3 create mode 100644 linhes/linhes-system/myth_mtc.cron create mode 100644 linhes/linhes-system/openssh.hook create mode 100644 linhes/linhes-system/paccache.cron create mode 100644 linhes/linhes-system/readme_is_xml create mode 100755 linhes/linhes-system/remove_storage.py create mode 100644 linhes/linhes-system/system-sudo.rules create mode 100644 linhes/linhes-system/xfs_defrag.cron diff --git a/linhes/linhes-system/10-monitor.conf b/linhes/linhes-system/10-monitor.conf new file mode 100644 index 0000000..f63cc86 --- /dev/null +++ b/linhes/linhes-system/10-monitor.conf @@ -0,0 +1,3 @@ +Section "Extensions" + Option "DPMS" "Disable" +EndSection diff --git a/linhes/linhes-system/79-cronie.hook b/linhes/linhes-system/79-cronie.hook new file mode 100644 index 0000000..59ca25e --- /dev/null +++ b/linhes/linhes-system/79-cronie.hook @@ -0,0 +1,9 @@ +[Trigger] +Operation = Install +Type = Package +Target = linhes-system + +[Action] +Description = Enable and start cronie... +When = PostTransaction +Exec = /usr/bin/systemctl enable --now cronie.service diff --git a/linhes/linhes-system/PKGBUILD b/linhes/linhes-system/PKGBUILD new file mode 100755 index 0000000..1674c61 --- /dev/null +++ b/linhes/linhes-system/PKGBUILD @@ -0,0 +1,87 @@ +pkgname=linhes-system +pkgver=9.0.0 +pkgrel=1 +arch=('x86_64') +#install=$pkgname.install +pkgdesc="Everything that makes LinHES a system" +license=('GPL2') +depends=('cronie' 'libnotify' 'linhes-templates' 'pacman-contrib' 'openssh' 'ttf-overlock') +binfiles="add_storage.py balance_storage_groups.py empty_storage_groups.py remove_storage.py + checkXFSfrag.sh enableIRWake.sh idle.py lh_systemstart.sh jobqueue_helper.py + find_orphans.py + create_media_dirs.sh be_check.py + myth2mkv myth2mp3" +source=($binfiles + 'myth_mtc.cron' 'paccache.cron' 'xfs_defrag.cron' + 'readme_is_xml' 'add_storage.readme' + '79-cronie.hook' 'fstrim.hook' 'openssh.hook' + '10-monitor.conf' + 'system-sudo.rules' + 'lh_systemstart.sh.desktop') + +package() { + cd $srcdir + + #bin files + BINDIR=$pkgdir/usr/bin + for i in $binfiles + do + item=$i + echo "installing $item to $BINDIR" + install -m755 -D $item $BINDIR/$item + done + + #startup files + install -D -m600 "$srcdir/lh_systemstart.sh.desktop" "$pkgdir/home/mythtv/.config/autostart/lh_systemstart.sh.desktop" + + #readme files + install -m644 -D $srcdir/readme_is_xml $pkgdir/etc/gen_is_xml.d/readme_is_xml + install -m644 -D $srcdir/readme_is_xml $pkgdir/etc/gen_lib_xml.d/readme_gen_xml + install -m644 -D $srcdir/readme_is_xml $pkgdir/etc/gen_game_xml.d/readme_gen_xml + install -m644 -D $srcdir/add_storage.readme $pkgdir/etc/storage.d/readme + + #cron files + install -m755 -D $srcdir/paccache.cron $pkgdir/etc/cron.weekly/paccache.cron + install -m755 -D $srcdir/xfs_defrag.cron $pkgdir/etc/cron.weekly/xfs_defrag + install -m755 -D $srcdir/myth_mtc.cron $pkgdir/etc/cron.hourly/myth_mtc + + #hooks + install -Dm0644 $srcdir/79-cronie.hook "${pkgdir}"/usr/share/libalpm/hooks/79-cronie.hook + install -Dm0644 $srcdir/fstrim.hook "${pkgdir}"/usr/share/libalpm/hooks/fstrim.hook + install -Dm0644 $srcdir/openssh.hook "${pkgdir}"/usr/share/libalpm/hooks/openssh.hook + + #sudo rules + mkdir -p $pkgdir/etc/sudoers.d/ + chmod 750 $pkgdir/etc/sudoers.d/ + chown -R root:root $pkgdir/etc/sudoers.d + install -o root -g root -m 0750 $srcdir/system-sudo.rules $pkgdir/etc/sudoers.d/system_sudo + + #disable dpms + install -m644 -D $srcdir/10-monitor.conf $pkgdir/etc/X11/xorg.conf.d/10-monitor.conf + +} +sha256sums=('96f67b5428debb7dac909893c56a7637bf6545c068732822981d4080125c53d9' + 'b0e8fe4d04e1f779d52a28156489fb51efc13e173efccba2d6d458044bf35904' + '4ab36bbabf0964f666278c225f4c2d41f4277acb42e9023163fa3a9599282466' + '632eb5073219b86667a361e567ad766fbd122f27dc5e775bba9983c417686aa9' + '11168c9cd3b117decaab6bc665c183b4aab917cf0a976bce4c1b5e4686a27bc9' + 'ae34515e144830f424d3bd3f6b1b446892d62beed20bca6f0fb19b0bbb779f27' + '23358a7bff4968eccd469613b81b1415c2ae0ebe77f14f74426697333e4d88d7' + '2cb8d676abc3b848d579aff2fe8796ca280b96abe40be03bc30055ce50d98c02' + '91bdec992bb2c933e15625c181f2195c402060b879168ebf35944cb064c904b9' + '76f023c0cde7fea269234f1b29c32b117b91769217d4b1b8a3922daceb25f9f8' + 'bffcc13e4b480f720feb2b3c781bc4247c63303250c3d885022c699573d45a33' + '0254a21644473ba7953501a223f13b9b55d7ec290c80a567724ca1ac13e02e30' + '9ea1b5583cd38f53bb79d9e4ccae91a028db0b6850162d7991b19122c564b9c9' + 'cab7724cd7c792adae79ba11cf59a0908edf197c437ae6b356ce86c6a365f0da' + '9e97b4d68c9e8988daacadd40f1de9f0b5945d870eba596a2ceb5e0c023fa9c0' + '186203d3c0520bb3d611da99d33a7713e9c1563814285f1f101097234f214b2f' + '4533e15b9141ab722f9b02c3ec7855f61c1e4d2deedd8591b1abd0ed551be581' + '5f502b1bc8815d69c802320790745e4526d5817fd8ecc7b00cf8b16078f8d440' + '12e424432bdf2d50afe3e632c018fef847e860a35a53525eccbe656b9c4118aa' + '0fbc05f521aea83157c5e6f8bd29a422873093bb6cded965cb7ffe98ff776fa4' + '4c29e0b71071ae9556cf2dbd75de560d028577fe5eb993113105112c4b445eac' + '890482242434e333024c7819e8bf3c889dc16548d0a1745479c8523930fb32f7' + 'dead17906b33a7f9d66ad13bb1c083a23438f45ece9bd5ec41ff86eda01c132a' + 'a9d0a94ff442453f0bec0b2e8afd591cf17b2845b6ae45ff300530114efd30af' + '7016c4bff7275e8d625cec418928ddeab0573df785dbaeda4ac710ddf64e9770') diff --git a/linhes/linhes-system/add_storage.py b/linhes/linhes-system/add_storage.py new file mode 100755 index 0000000..041f612 --- /dev/null +++ b/linhes/linhes-system/add_storage.py @@ -0,0 +1,1185 @@ +#!/usr/bin/python +#add_storage.py used to auto add new storage to MythTV storage groups +#If it's a new disk it will erase the entire disk and reformat. +# +#Disks that are mounted, in fstab, size < 5000 bytes, optical or +#have already been seen will not be presented as an option. +# + + +import dbus +import pickle +import subprocess +import sys,os +import random, string +import configparser +from configparser import SafeConfigParser +import glob + +from MythTV import MythDB, MythBE, Recorded, MythError +from socket import timeout, gethostname + + +storage_dir = "/etc/storage.d" +pickle_file = "%s/storage.pkl" %storage_dir + + +SG_MAP={ + 'Default' :'media/tv/', + 'LiveTV' :'media/tv/live/', + 'DB Backups' :'backup/mythtv_backups/', + 'Music' :'media/music/', + 'Streaming' :'media/streaming/', + 'Videos' :'media/video/', + 'Photographs':'media/photos/', + 'Banners' :'media/artwork/banners/', + 'Coverart' :'media/artwork/coverart/', + 'Fanart' :'media/artwork/fanart/', + 'MusicArt' :'media/artwork/musicart/', + 'Screenshots':'media/artwork/screenshots/', + 'Trailers' :'media/artwork/trailers/', + } + +FS_LIST=[] +for key in list(SG_MAP.keys()): + FS_LIST.append(SG_MAP[key]) + +class disk_device: + def __init__(self,device,storage_dir): + device_obj = bus.get_object("org.freedesktop.UDisks", device) + device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) + self.storage_dir = storage_dir + self.top_mount_dir = "/data/storage" + self.config = configparser.RawConfigParser() + self.fs_map = self.get_fsmap() + self.is_device = self.get_is_device(device_props) + self.vendor = self.get_vendor(device_props) + self.model = self.get_model(device_props) + + self.mmount = False + self.dir_sg = False + self.f = self.get_mounts(device_props) + self.is_mounted = self.get_is_mounted(device_props) + + self.parition_size = self.get_partition_size(device_props) + self.device_size = self.get_device_size(device_props) + self.serial_number = self.get_serial_number(device_props) + self.read_only = self.get_is_readonly(device_props) + self.is_optical = self.get_is_optical_disc(device_props) + self.connection = self.get_connections(device_props) + self.block_path = self.get_device_file(device_props) + self.device_file_path = self.get_device_file_path(device_props) + self.block_partition="%s1" %self.block_path + self.in_use = self.get_in_use() + self.uuid='' + self.new_mount_point='' + self.disk_num='' + + def set_partition(self,partition): + self.block_partition = "%s%s" %(self.block_path,partition) + + def set_mmount(self,flag): + self.mmount = flag + + def set_dir_sg(self,flag): + self.dir_sg = flag + + def set_disk_num(self,num): + self.disk_num=num + + def get_is_device(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsDrive") + + def get_vendor(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DriveVendor") + + def get_model(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DriveModel") + + def get_mounts(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceMountPaths") + + def get_is_mounted(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsMounted") + + def get_name(self): + filename="%s_%s" %(self.model.replace(' ',''), + self.serial_number.replace(' ','')) + return filename + + def get_in_use(self): + in_use = False + for i in self.fs_map: + if self.block_path in i[0]: + in_use = True + break + return in_use + + def get_partition_size(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "PartitionSize") + + def get_device_size(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceSize") + + + def get_serial_number(self,device_props): + serial_number = device_props.Get('org.freedesktop.UDisks.Device', "DriveSerial") + random_string = os.urandom(5) + if serial_number == '': + serial_number = "".join( [random.choice(string.letters) for i in range(6)] ) + serial_number = "NSW%s" %serial_number + return serial_number + + def get_is_readonly(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsReadOnly") + + def get_is_optical_disc(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsOpticalDisc") + + def get_connections(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DriveConnectionInterface") + + def get_device_file(self,device_props): + return device_props.Get('org.freedesktop.UDisks.Device', "DeviceFile") + + def get_device_file_path(self,device_props): + path = device_props.Get('org.freedesktop.UDisks.Device', "DeviceFileByPath") + try: + path = path[0] + except: + path = "None" + + return path + + + def partition_disk(self): + print(" Creating new partition table") + cmd = "parted -s -a optimal %s mklabel gpt" %self.block_path + runcmd(cmd) + cmd = "parted -s -a optimal %s mkpart primary \" 1 -1\"" %self.block_path + runcmd(cmd) + return + + def get_fsmap(self): + #block,point,fs + fs_map=[] + + f = open('/proc/mounts','r') + mounts=f.readlines() + f.close() + for line in mounts: + temp_fs=[] + split_line=line.split() + block=split_line[0] + mountp=split_line[1] + fs=split_line[2] + block=os.path.realpath(block) + if block.startswith("/dev"): + temp_fs.append(block) + temp_fs.append(mountp) + temp_fs.append(fs) + fs_map.append(temp_fs) + return fs_map + + def find_fstype(self,moutpoint): + fstype="xfs" + mp=['/myth', '/data/storage/disk0'] + for i in self.fs_map: + if i[1] in mp: + fstype = i[2] + break + return fstype + + def lookup_format(self): + fstab = self.read_fstab() + current_media_mount = self.find_options_type(fstab)[1] + new_fstype = self.find_fstype(current_media_mount) + #setting self.new_fstype so that it can be referenced when setting fstab + self.new_fstype = new_fstype + return + + def format_disk(self): + #lookup format + #self.lookup_format() + #do format + if self.new_fstype == "xfs": + cmd = "mkfs -t %s -f %s " %(self.new_fstype,self.block_partition) + else: + cmd = "mkfs -t %s %s " %(self.new_fstype,self.block_partition) + print(" Formatting %s with %s" %(self.block_partition,self.new_fstype)) + runcmd(cmd) + return + + def find_uuid(self,partition): + #logging.info("Finding the UUID for %s...", partition) + cmd = "blkid -s UUID %s" %partition + tmpuuid = runcmd(cmd)[1] + splituuid = tmpuuid.partition("=") + uuid = splituuid[2].replace('"', "") + # logging.info("The uuid is %s", uuid) + if uuid == '': + print("Could not find a UUID for device: %s" %partition) + sys.exit(1) + self.uuid = uuid.strip() + return uuid.strip() + + def read_fstab(self): + f = open('/etc/fstab', 'r') + fstab=f.readlines() + f.close() + return fstab + + def find_options_type(self,fstab): + mp=['/myth', '/data/storage/disk0'] + for i in fstab: + split_line=i.split() + try: + if split_line[1] in mp: + options = split_line[3] + break + else: + options = "defaults" + mount_point = i + except: + options = "defaults" + return options,i + + + def add_fstab(self,bind=False): + new_fstab_list=['UUID=', 'mount_point', 'auto', 'defaults', '0', '1'] + fstab=self.read_fstab() + new_fstab=[] + + #determine mount_path + self.new_mount_point="%s/%s_%s" %(self.top_mount_dir,self.model.replace(' ',''),self.serial_number.replace(' ','')) + + if bind: + new_fstab_list=["/data/storage/disk0" , self.new_mount_point , "none" , "rw,bind", '0', '0'] + uuid=self.find_uuid(self.block_partition) + else: + #check for old mount point and comment out + for line in fstab: + if not line.startswith("#"): + if line.find(self.new_mount_point) > -1: + print(" Found old mount %s in fstab, commenting out" %self.new_mount_point) + line = "#"+line + new_fstab.append(line) + fstab=new_fstab + + #determine options + new_options = self.find_options_type(fstab)[0] + + #find blkid + #self.block_partition="%s1" %self.block_path + uuid=self.find_uuid(self.block_partition) + + #construct new line + new_fstab_list[0]="UUID=%s" %uuid + new_fstab_list[1]=self.new_mount_point + new_fstab_list[3]=new_options + + new_fstab_line='\t'.join(new_fstab_list) + new_fstab_line="%s\n" %new_fstab_line + fstab.append(new_fstab_line) + + #add line to fstab + f = open('/etc/fstab', 'w') + #f = open('/tmp/fstab', 'w') + for i in fstab: + f.write(i) + #f.write("\n") + f.close() + return + + def mount_disk(self,no_mount=False): + try: + os.stat(self.new_mount_point) + except: + os.makedirs(self.new_mount_point) + if no_mount == False: + if os.path.ismount(self.new_mount_point): + print(" Disk already mounted, will not mount:\n %s" %self.new_mount_point) + pass + else: + print(" Mounting %s" %self.new_mount_point) + cmd = "mount %s" %self.new_mount_point + runcmd(cmd) + return + + def mkdirs(self,FS_LIST): + print(" Creating directory structure:") + print(" %s" %self.new_mount_point) + for y in FS_LIST: + print(" %s" %y) + new_dir="%s/%s" %(self.new_mount_point,y) + try: + os.stat(new_dir) + except: + os.makedirs(new_dir) + cmd="chown -R mythtv:mythtv /%s" %self.new_mount_point + runcmd(cmd) + cmd="chmod -R 775 /%s" %self.new_mount_point + runcmd(cmd) + + def add_sg(self,DB,host,SG_MAP,weight='0',install_call=False): + print(" Adding storage groups") + sgweight=int(weight) + for key in SG_MAP.keys(): + #print key," : ", SG_MAP[key] + gn=key + hn=host + dn="%s/%s" %(self.new_mount_point,SG_MAP[key]) + #print dn + #print gn + #print hn + if install_call == True : + print("Will write SG for stuff after the fact") + else: + with DB as c: + #delete old dir without trailing slash + c.execute("""delete from storagegroup where groupname = %s and hostname = %s and dirname = %s""", (gn,hn,dn.rstrip('/'))) + + try: + c.execute("""insert into storagegroup (groupname,hostname,dirname) values (%s,%s,%s)""",(gn,hn,dn)) + print(" Adding location: %s to storagegroup %s" %(dn,gn)) + except: + print(" *Error inserting %s into storage groups" %dn) + + if sgweight > 0: + try: + #("SGweightPerDir:server2:/mnt/video", 99, "server2"); + sgw="SGweightPerDir:%s:%s" %(hn,dn) + #print sgw + #print sgweight + #print hn + + #delete old dir without trailing slash + c.execute("""delete from settings where value = %s and data = %s and hostname = %s""", (sgw.rstrip('/'),sgweight,hn)) + + c.execute("""insert into settings (value,data,hostname) values (%s,%s,%s)""",(sgw,sgweight,hn)) + print(" Adding storage group weight of %s for %s\n" %(sgweight,gn)) + except: + print(" *Error setting storage group weight %s for %s\n" %(sgweight,gn)) + + return + + def write_config(self): + print(" Writing /etc/storage.d conf file") + self.config.add_section('storage') + self.config.set('storage','uuid',self.uuid) + self.config.set('storage','mountpoint',self.new_mount_point) + self.config.set('storage','fstype',self.new_fstype) + self.config.set('storage','shareable','True') + self.config.set('storage','mmount',self.mmount) + self.config.set('storage','storage_groups',self.dir_sg) + self.config.set('storage','disk_num',self.disk_num) + + + filename="%s_%s.conf" %(self.model.replace(' ',''), + self.serial_number.replace(' ','')) + + configfile="/etc/storage.d/%s" %filename + print(" %s" %configfile) + with open(configfile, 'wb') as configfile: + self.config.write(configfile) + return + + def symlink_disk(self): + print(" Creating symlink for disk%s" %self.disk_num) + disk_ln="%s/disk%s" %(self.top_mount_dir,self.disk_num) + cmd = "ln -s %s %s" %(self.new_mount_point,disk_ln) + runcmd(cmd) + + def symlink(self): + pass + print(" Creating symlink for /myth") + if not os.path.exists("/myth"): + cmd = "ln -s %s/media /myth " %(self.new_mount_point) + runcmd(cmd) + else: + print(" Skipping symlink, /myth already exists") + + +#end of class + + +def runcmd(cmd): + if True : + pass + else: + cmd = "echo "+cmd + #print cmd + cmdout = subprocess.getstatusoutput(cmd) + #logging.debug(" %s", cmdout) + return cmdout + + + + +def scan_system(): + + ud_manager_obj = bus.get_object("org.freedesktop.UDisks", "/org/freedesktop/UDisks") + ud_manager = dbus.Interface(ud_manager_obj, 'org.freedesktop.UDisks') + current_drive_list=[] + for dev in ud_manager.EnumerateDevices(): + drive = disk_device(dev,storage_dir) + + if drive.is_device and drive.device_size > 5000 and not drive.is_optical : + current_drive_list.append(drive) + return current_drive_list + +def read_known_list(): + #reading pickle file + known_drive_list=[] + try: + pkl_file = open(pickle_file, 'rb') + known_drive_list = pickle.load(pkl_file) + pkl_file.close() + except: + pass + return known_drive_list + + +def write_known_drive_list(known_drive_list): + output = open(pickle_file, 'wb') + pickle.dump(known_drive_list, output, -1) + output.close() + + +def search_for_match(system_drive,known_drive_list): + match_drive=False + for y in known_drive_list: + if system_drive.serial_number.startswith("NSW"): + #print "Match_test: hash" + system_drive_hash = "%s_%s_%s" %(system_drive.model,system_drive.parition_size,system_drive.device_file_path) + y_drive_hash = "%s_%s_%s" %(y.model,y.parition_size,y.device_file_path) + if system_drive_hash == y_drive_hash : + match_drive = True + print("\n* No serial number was found, matched using hash method: %s" %system_drive.model) + break + + elif y.serial_number == system_drive.serial_number: + #print "Match_test: serial number" + match_drive=True + break + + return match_drive + + +def prompt_to_add(current_drive,destruction = True): + loop = True + if destruction : + prompt = ''' + ** Adding this disk will remove all contents on the disk. ** + This disk will be partitioned and formatted. + + Enable this disk for storage (Y/N)?:''' + else: + prompt = ''' + ** Preserving existing contents on the disk. ** + This disk will NOT be partitioned or formatted. + + Enable this disk for storage (Y/N)?:''' + while loop: + str1 = input(prompt) + + if str1 in ['Y','N','y','n']: + loop = False + break + print("\n") + if str1 == 'Y' or str1 == 'y': + rc = True + else: + rc = False + return rc + +def prompt_to_continue(process_list): + loop = True + print("\n\n\n Ready to add additional storage!\n") + if destruction: + print("** WARNING: These disk(s) WILL be partitioned and formatted. **\n ** All content on these disk(s) will be erased. **") + else: + print(" ** These disk(s) will NOT be partitioned and formatted. **") + for i in process_list: + print(" %s" %(i.get_name())) + str1 = input("\n Press Y to add disk(s), any other key to cancel:") + + if str1 == 'Y' or str1 == 'y': + rc = True + else: + rc = False + print("\nCancelled: No disk(s) added to your system.") + print("-----") + return rc + +def prompt_sg(dir_sg): + #check for storage groups + print("*" * 60) + if dir_sg != True: + loop = True + prompt_string=''' + MythTV storage groups are used for artwork, database backups, + photos, music, streaming, TV recordings, and videos. + + The content on these storage groups will + only be available while the system is online. + + Enabling MythTV storage groups will create the directories + on the disk(s) and add the paths to the MythTV database. + + Enable MythTV storage groups (Y/N)?:''' + + while loop: + str1 = input(prompt_string) + if str1 in ['Y','N','y','n']: + loop = False + break + print("\n") + + if str1 == 'Y' or str1 == 'y': + dir_sg = True + print(" ** Will add MythTV storage groups!") + else: + print(" ** Will NOT add MythTV storage groups!") + dir_sg = False + else: + dir_sg = True + print("\n --add_sg option used") + print(" ** Will add MythTV storage groups!") + + return dir_sg + +def remove_pickle(): + try: + print("\n* Removing list of known disks.\n") + os.remove(pickle_file) + except: + pass + +def last_disk_num(): + parser = SafeConfigParser() + num_list = [] + for conf_file in glob.glob('%s/*.conf' %storage_dir): + parser.read(conf_file) + disk_num = parser.get('storage', 'disk_num') + num_list.append(int(disk_num)) + num_list.sort() + try: + return num_list[-1] + except: + # conf file or disk_num is missing so fallback to /data/storage/disk# links + for disk_name in glob.glob('/data/storage/disk*'): + disk_num = disk_name.strip('/data/storage/disk') + num_list.append(int(disk_num)) + num_list.sort() + try: + return num_list[-1] + except: + print("Couldn't find last disk number.") + sys.exit(1) + +#-------------------------------------------- + +def main(scan_only, destruction, no_mount, install_call, dir_sg): + global bus + bus = dbus.SystemBus() + + system_drive_list = scan_system() + known_drive_list=[] + known_drive_list = read_known_list() + process_list=[] + no_process_list=[] + + print("-" * 60) + print(" Scan for Disks") + + for i in system_drive_list: + #print i.mount_path + #print i.is_mounted + #print i.in_use + #print i.model + #print i.block_path + #print "--" + + if search_for_match(i,known_drive_list) or i.in_use : + if search_for_match(i,known_drive_list) : + dstatus = " Ignoring - Disk has been previously skipped:" + if i.in_use : + dstatus = " Ignoring - Disk is mounted:" + if search_for_match(i,known_drive_list) and i.in_use : + dstatus = " Ignoring - Disk has been previously skipped and is mounted:" + + print("\n") + print(" --------------------------------------------------------") + print(dstatus) + print(" model: %s" %i.model) + print(" location: %s" %i.block_path) + print(" size: %s" %i.device_size) + continue + + else: + if not scan_only: + print("\n") + print(" --------------------------------------------------------") + print(" Found New Disk:") + print(" model: %s" %i.model) + print(" location: %s" %i.block_path) + print(" size: %s " %i.device_size) + + if prompt_to_add(i,destruction) : + print("\n %s will be added to your system!" %i.model) + process_list.append(i) + else: + no_process_list.append(i) + else: + process_list.append(i) + print("\n") + print(" Scan Finished") + print("-" * 60) + + if scan_only: + if len(process_list) > 0: + print(" Unknown or Unmounted Disks:") + f = open('/tmp/scan_report', 'w') + for i in process_list: + f.write("disk: %s , location: %s ,size: %s \n" %(i.model,i.block_path,i.device_size)) + print("\n") + print(" ---------------------------------------------------------") + print(" Found New Disk:") + print(" model: %s" %i.model) + print(" location: %s" %i.block_path) + print(" size: %s " %i.device_size) + f.close() + sys.exit(0) + + for i in no_process_list: + system_drive_list.append(i) + + + if len(process_list) > 0: + DB = MythDB() + host=gethostname() + for y in process_list: + system_drive_list.remove(y) + write_known_drive_list(system_drive_list) + else: + print("\nThere are no disks to add to your system.\n\nFor more options: add_storage.py --help\n") + write_known_drive_list(system_drive_list) + #BE = MythBE(db=DB) + + + #save new list to disk_device + #write_known_drive_list(system_drive_list) + #disk_num = last_disk_num() + + + if len(process_list) > 0: + print("\n Will add %s disk(s) to your system." %len(process_list)) + + dir_sg = prompt_sg(dir_sg) + if prompt_to_continue(process_list) == True: + write_known_drive_list(system_drive_list) + disk_num = last_disk_num() + for i in process_list: + print(" Disk: %s" %(i.get_name())) + disk_num = disk_num + 1 + i.lookup_format() + if destruction == True: + i.partition_disk() + i.format_disk() + i.add_fstab() + i.mount_disk(no_mount) + #if destruction == True: + + if dir_sg == True: + i.mkdirs(FS_LIST) + + i.set_disk_num(disk_num) + i.set_dir_sg(dir_sg) + i.write_config() + system_drive_list.append(i) + write_known_drive_list(system_drive_list) + + i.symlink_disk() + + if dir_sg == True: + i.add_sg(DB,host,SG_MAP) + + print("-----") + cmd = "systemconfig.py -m fileshare" + runcmd(cmd) + #i.add_sg(DB,host,SG_MAP) + +def myth_main(no_mount,install_call,dir_sg): + global bus + bus = dbus.SystemBus() + #search for root + f = open('/etc/fstab', 'r') + fstab=f.readlines() + f.close() + for i in fstab: + split_line=i.split() + if not split_line: + continue + + try: + if split_line[1] == "/" : + uuid_device = split_line[0] + break + except: + print("Couldn't find / in fstab") + sys.exit(1) + if uuid_device.startswith("UUID"): + uuid_device = uuid_device.split("=")[1] + cmd = "blkid -U %s" %uuid_device + device = runcmd(cmd)[1] + else: + device = uuid_device.strip() + #should have something like /dev/sda1 + #remove all the digits + device = ''.join([letter for letter in device if not letter.isdigit()]) + + + system_drive_list = scan_system() + for i in system_drive_list: + if i.block_path == device: + break + else: + print("Couldn't find root device in block list") + sys.exit(1) + if not install_call == True: + DB = MythDB() + host=gethostname() + else: + DB = None + host=gethostname() + + + print(" Disk: %s" %(i.get_name())) + i.set_mmount(True) + i.set_dir_sg(dir_sg) + i.set_partition("7") + i.set_disk_num(0) + i.lookup_format() + i.add_fstab(True) + #if not install_call: + i.mount_disk(no_mount) + i.write_config() + #no need to make the sub directories because the install process has taken care of it. + if dir_sg == True: + i.add_sg(DB,host,SG_MAP,'99',install_call) + + i.symlink() + cmd = "systemconfig.py -m fileshare" + runcmd(cmd) + +def reconstruct_storagegroups(): + print("\nRecreating storage groups from contents of /etc/storage.d/\n") + + DB = MythDB() + host=gethostname() + + for conf_file in glob.glob('%s/*.conf' %storage_dir): + parser = SafeConfigParser() + parser.read(conf_file) + mount_point = parser.get('storage', 'mountpoint') + mmount = parser.getboolean('storage', 'mmount') + try: + removed = parser.getboolean('storage', 'removed') + except: + removed = False + if removed: + print("Skipping: " + mount_point + " - removed") + continue + if not os.path.ismount(mount_point): + print("Skipping: " + mount_point + " - not mounted") + continue + try: + dir_sg = parser.getboolean('storage', 'storage_groups') + except configparser.NoOptionError as err: + print("SG not found in conf, get setting from DB") + dir_sg = False + # Get storage sroup directories from DB + recs = DB.getStorageGroup(groupname="Default") + for record in recs: + if record.dirname.startswith(mount_point): + dir_sg = True + # Write SG usage to conf + parser.set('storage','storage_groups',str(dir_sg)) + with open(conf_file, 'wb') as conf_file: + parser.write(conf_file) + + if dir_sg is True: + print("SGs Enabled for: " + mount_point) + print(" Creating directory structure:") + print(" %s" %mount_point) + for y in FS_LIST: + new_dir="%s/%s" %(mount_point,y) + try: + os.stat(new_dir) + print(" %s - exists" %y) + except: + os.makedirs(new_dir) + cmd="chown -R mythtv:mythtv /%s" %new_dir + runcmd(cmd) + cmd="chmod -R 775 /%s" %new_dir + runcmd(cmd) + print(" %s - created" %y) + + print(" Adding storage groups to DB") + if mmount is True: + sgweight=99 + else: + sgweight=0 + + for key in SG_MAP.keys(): + gn=key + hn=host + dn="%s/%s" %(mount_point,SG_MAP[key]) + with DB as c: + #delete old dir without trailing slash + c.execute("""delete from storagegroup where groupname = %s and hostname = %s and dirname = %s""", (gn,hn,dn.rstrip('/'))) + + try: + c.execute("""insert into storagegroup (groupname,hostname,dirname) values (%s,%s,%s)""",(gn,hn,dn)) + print(" Added: %s to storagegroup %s" %(dn,gn)) + except: + print(" Skipping: %s exists" %dn) + if sgweight > 0: + try: + sgw="SGweightPerDir:%s:%s" %(hn,dn) + + #delete old dir without trailing slash + c.execute("""delete from settings where value = %s and data = %s and hostname = %s""", (sgw.rstrip('/'),sgweight,hn)) + + if DB.settings[hn][sgw] == '99': + print(" Skipping: storage group weight DB entry exists") + else: + c.execute("""insert into settings (value,data,hostname) values (%s,%s,%s)""",(sgw,sgweight,hn)) + print(" Adding storage group weight of %s for %s\n" %(sgweight,gn)) + except: + print(" *Error setting storage group weight %s for %s\n" %(sgweight,gn)) + + else: + print("SGs Disabled for: " + mount_point) + return + +class reconstruct_path: + def __init__(self,conf_file): + self.conf_file = conf_file + parser = SafeConfigParser() + parser.read(self.conf_file) + self.config = configparser.RawConfigParser() + + self.uuid = parser.get('storage', 'uuid') + self.mount_point = parser.get('storage', 'mountpoint') + self.shareable = parser.get('storage', 'shareable') + self.myth_mount = parser.get('storage', 'mmount') + self.bind = self.myth_mount + self.disk_num = parser.get('storage', 'disk_num') + self.top_mount_dir = os.path.dirname(self.mount_point) + try: + self.fstype = parser.get('storage', 'fstype') + except: + self.fstype = self.get_fstype() + try: + self.removed = parser.get('storage', 'removed') + except: + self.removed = False + + def get_fstype(self): + cmd = "fsck -N UUID=%s" %self.uuid + tmpfstype = runcmd(cmd) + tmpfstype = tmpfstype[1].split('/sbin/fsck.') + tmpfstype = tmpfstype[1].split(' ') + self.fstype = tmpfstype[0] + self.write_config() + return self.fstype + + def get_conf(self): + return self.conf_file + + def get_uuid(self): + return self.uuid + + def get_mount_point(self): + return self.mount_point + + def get_shareable(self): + return self.shareable + + def get_is_myth_mount(self): + return self.myth_mount + + def get_disk_num(self): + return self.disk_num + + def get_removed(self): + return self.removed + + def create_mount_point(self): + try: + os.stat(self.mount_point) + except: + os.makedirs(self.mount_point) + + def find_options_type(self,fstab): + mp=['/myth', '/data/storage/disk0'] + for i in fstab: + split_line=i.split() + try: + if split_line[1] in mp: + options = split_line[3] + break + else: + options = "defaults" + mount_point = i + except: + options = "defaults" + return options,i + + def read_fstab(self): + f = open('/etc/fstab', 'r') + fstab=f.readlines() + f.close() + return fstab + + def check_in_fstab(self,fstab,check_path): + for line in fstab: + if line.find(check_path) > -1: + return True + return False + + def append_fstab(self,line): + new_fstab_line='\t'.join(line) + new_fstab_line="%s\n" %new_fstab_line + + f = open('/etc/fstab', 'a') + #f = open('/tmp/fstab', 'a') + f.write(new_fstab_line) + #f.write("\n") + f.close() + + def symlink(self): + print(" Creating symlink for /myth") + if not os.path.exists("/myth"): + cmd = "ln -s %s/media /myth " %(self.mount_point) + runcmd(cmd) + else: + print(" Skipping symlink, /myth already exists") + + def symlink_disk(self): + print(" Creating symlink for disk%s" %self.disk_num) + disk_ln="%s/disk%s" %(self.top_mount_dir,self.disk_num) + cmd = "ln -s %s %s" %(self.mount_point,disk_ln) + runcmd(cmd) + + def add_fstab(self): + #new_fstab_list=['UUID=', 'mount_point', 'auto', 'defaults', '0', '1'] + new_fstab_list=['UUID=', 'mount_point', self.fstype, 'defaults', '0', '1'] + fstab=self.read_fstab() + + if self.bind == "True": + self.symlink() + + if self.check_in_fstab(fstab,self.uuid) == True: + print(" Found UUID of disk in fstab, will not add it") + else: + print(" Adding storage to fstab") + if self.bind == "True" : + print(" Bind mount") + new_fstab_list=["/data/storage/disk0" , self.mount_point , "none" , "rw,bind", '0', '0'] + if self.check_in_fstab(fstab,self.mount_point) == False: + self.append_fstab(new_fstab_list) + else: + print(" Found bind storage in fstab, will not add it") + + else: + #check for old mount point and comment out + f = open('/etc/fstab', 'w') + for line in fstab: + if not line.startswith("#"): + if line.find(self.mount_point) > -1: + print(" Found old mount %s in fstab, commenting out" %self.mount_point) + line = "#"+line + f.write(line) + f.close() + + #construct new line + new_options = self.find_options_type(fstab)[0] + new_fstab_list[0]="UUID=%s" %self.uuid + new_fstab_list[1]=self.mount_point + new_fstab_list[3]=new_options + self.append_fstab(new_fstab_list) + + def mount_disk(self,no_mount=False): + try: + os.stat(self.mount_point) + except: + os.makedirs(self.mount_point) + if no_mount == False : + if os.path.ismount(self.mount_point): + print(" Disk already mounted, will not mount:\n %s" %self.mount_point) + pass + else: + print(" Mounting %s" %self.mount_point) + cmd = "mount %s" %self.mount_point + runcmd(cmd) + return + + def write_config(self): + print(" Writing /etc/storage.d conf file") + self.config.add_section('storage') + self.config.set('storage','uuid',self.uuid) + self.config.set('storage','mountpoint',self.mount_point) + self.config.set('storage','fstype',self.fstype) + self.config.set('storage','shareable','True') + self.config.set('storage','mmount',self.myth_mount) + self.config.set('storage','storage_groups',self.dir_sg) + self.config.set('storage','disk_num',self.disk_num) + + print(" %s" %self.conf_file) + with open(self.conf_file, 'wb') as self.conf_file: + self.config.write(self.conf_file) + return + +def reconstruct_mounts(no_mount): + print("\nRecreating disks from contents of /etc/storage.d/") + for conf_file in glob.glob('%s/*.conf' %storage_dir): + print("\n") + cf = reconstruct_path(conf_file) + + # skip if the disk was removed + if cf.get_removed(): + continue + #print cf.get_conf() + #print cf.get_uuid() + print(" Recreating %s" %cf.get_mount_point()) + #print cf.get_shareable() + #print cf.get_is_myth_mount() + #print cf.get_disk_num() + + cf.create_mount_point() + cf.add_fstab() + cf.symlink_disk() + cf.mount_disk(no_mount) + + print("\n\nDone recreating disks.\n") + pass + + +def usage(): + help=''' + add_storage.py finds and sets up disks for MythTV usage. + It's a powerful tool that could destroy data if not used correctly, + please be careful. + + Scanned disks are ignored if they are mounted or have been + previously skipped by add_storage.py. + + The file system type for disks added by add_storage.py is + automatically set to the type selected for the data partition + at install. + + Normal operations without options include (in this order): + Partition the disk + Format the disk + Add disk to /etc/fstab + Mount the disk + Create the directories + (if user enables MythTV storage groups) + Write out the disk config file to /etc/storage.d/ + Create disk# symlink at /data/storage/ + Create /myth symlink (if applicable) + Create MythTV storage group paths in MythTV database + (if user enables MythTV storage groups) + + Options: + --add_sg: Create the MythTV storage group directories and + database entries for database backups, TV + recordings, photos, music, streaming, videos + and artwork. + -h, --help: Show this help message. + --new_init: Erase the list of known disks and rescan. + --no_destruction: Will not partition or format the disk. + All other normal operations will be performed. + Can be used to import disks from other systems + however, add_storage.py only works with the first + partition on a disk and ignores all others. + --no_mount: Do not mount the disk. + All other normal operations will be performed. + --reconstruct: Recreate mount point, /myth symlink, fstab entry, + /data/storage/disk# symlink, and mount the disk. + --no_mount is the only option that works with + --reconstruct. + --reconstruct_sg: Recreate the MythTV storage group directories and + database entries if they don't exist. + No other options work with --reconstruct_sg. + --report: Scan disks and print new found disks. + ''' + print(help) + sys.exit(0) + + + + +if __name__ == "__main__": + scan_only = False + myth_mount = False + no_mount = False + destruction = True + install_call = False + dir_sg = False + reconstruct = False + reconstruct_sg = False + try: + os.remove("/tmp/scan_report") + except: + pass + + if not os.geteuid()==0: + sys.exit("\nRoot access is required to run this program.\n") + + if "--help" in sys.argv or "-h" in sys.argv: + usage() + + if "--install_call" in sys.argv: + install_call = True + + if "--no_mount" in sys.argv : + no_mount = True + + if "--no_destruction" in sys.argv: + destruction = False + + if "--new_init" in sys.argv : + remove_pickle() + + if "--report" in sys.argv : + scan_only = True + + if "--add_sg" in sys.argv: + dir_sg = True + + #there is no distinction between FE and BE sg anymore + #but leaving these for backwards compatibility + if "--add_fe_sg" in sys.argv: + dir_sg = True + + if "--add_be_sg" in sys.argv: + dir_sg = True + + if "--reconstruct" in sys.argv: + reconstruct = True + + if "--reconstruct_sg" in sys.argv: + reconstruct_sg = True + + if "--double_myth" in sys.argv: + myth_main(no_mount, install_call, dir_sg) + elif reconstruct == True: + reconstruct_mounts(no_mount) + elif reconstruct_sg == True: + reconstruct_storagegroups() + else: + main(scan_only, destruction, no_mount, install_call, dir_sg) diff --git a/linhes/linhes-system/add_storage.readme b/linhes/linhes-system/add_storage.readme new file mode 100644 index 0000000..d4435d5 --- /dev/null +++ b/linhes/linhes-system/add_storage.readme @@ -0,0 +1,3 @@ +Files here are autogenerated by add_storage.py. +They will be used by systemconfig to generate nfs, smb and mountpoint recovery. + diff --git a/linhes/linhes-system/balance_storage_groups.py b/linhes/linhes-system/balance_storage_groups.py new file mode 100755 index 0000000..0dd4b01 --- /dev/null +++ b/linhes/linhes-system/balance_storage_groups.py @@ -0,0 +1,153 @@ +#!/usr/bin/python + +import argparse, glob, operator, os, random, shutil, subprocess, sys, signal +shouldQuit = False + +def getFreeSpaceForDir(dir): + stats = os.statvfs(dir) + return (stats.f_bavail * stats.f_frsize) + +def getFreePercentForDir(dir): + stats = os.statvfs(dir) + total = (stats.f_blocks) + avail = (stats.f_bavail) + return (total - avail) / float(total) + +def getFileSize(fullPath): + return os.path.getsize(fullPath) + +def sizeof_fmt(num, suffix='B'): + for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: + if abs(num) < 1024.0: + return "%3.1f %s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f %s%s" % (num, 'Yi', suffix) + +def signal_handler(signal, frame): + print("\nWill quit when file has been moved.\nMoving File...") + global shouldQuit + shouldQuit = True + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--checkonly', action='store_true', help="Check only, don't move any files.") + parser.add_argument('-p', '--percent', type=int, default=7, help="The percentage difference between the most full dir and least full dir that will stop balancing.") + cmdargs = parser.parse_args() + + SGDIRS = [] + SGgrp = "Default" + + signal.signal(signal.SIGINT, signal_handler) + + print("\nBalance MythTV Storage Group Directories\nPress Ctrl+C to quit") + + # Get Storage Groups from MythDB + try: + from MythTV import MythDB + mythDB = MythDB() + records = mythDB.getStorageGroup() + except: + print("Couldn't connect to MythTV database.") + sys.exit(1) + + # Get Storage Group directories + for record in records: + if record.groupname == SGgrp: + dirname = record.dirname + SGDIRS.append(dirname) + + # If there are less than 2 directories defined bail as we can't move anything + if len(SGDIRS) < 2: + print("There are less than 2 directories defined. Exiting.") + sys.exit(0) + + while not shouldQuit: + SGDIRSdata = [] + print("\n------------------------------------------------") + print("'" + SGgrp + "' Storage Group Directories - Percent Used:") + # Get percent free and size free + for directory in SGDIRS: + # Check if SG path exists + if not os.path.exists(directory): + print(" " + directory + " - Not Mounted") + continue + freePcent = getFreePercentForDir(directory) + freeSize = getFreeSpaceForDir(directory) + SGDIRSdata.append([directory, freePcent, freeSize]) + print(" %s - %.2f%%" % (directory, freePcent * 100)) + + # Sort data on percent free + SGDIRSdata = sorted(SGDIRSdata, reverse=True, key=operator.itemgetter(1)) + #print SGDIRSdata + + # Check if SG has any ts, mpg or nuv files + i=0 + for dir in SGDIRSdata: + mostFull = SGDIRSdata[i] + i=i+1 + if len(glob.glob1(mostFull[0],"*.ts")) or len(glob.glob1(mostFull[0],"*.mpg")) or len(glob.glob1(mostFull[0],"*.nuv")): + break + else: + if i == 1: + print("------------------------------------------------") + print(" " + mostFull[0] + " - NO files to move") + + leastFull = SGDIRSdata[-1] + + print("------------------------------------------------") + print("Most Used Storage Group Directory with files to move: ") + print(" %s - %.2f%%" % (mostFull[0], mostFull[1] * 100)) + print("Least Used Storage Group Directory: ") + print(" %s - %.2f%%" % (leastFull[0], leastFull[1] * 100)) + + # Check if mostFull and leastFull are within the percent var of each other + if mostFull[1] - (float(cmdargs.percent) / 100) < leastFull[1]: + print("\nThe most used and least used storage group directories are\nwithin " + str(cmdargs.percent) + "% used of each other. No files will be moved.") + sys.exit() + + # Get random file from most used dir + fileToMove = random.choice([f for f in os.listdir(mostFull[0]) if f.endswith(".ts") or f.endswith(".mpg") or f.endswith(".nuv")]) + filePathToMove = mostFull[0] + "/" + fileToMove + + # Check that the file isn't too big for least used dir + fileSize = getFileSize(filePathToMove) + if (fileSize > getFreeSpaceForDir(leastFull[0])): + # Too big to move + print(filePathToMove + " is too big to move to " + leastFull[0]) + sys.exit() + + print("------------------------------------------------") + print("Move File:") + print(" " + filePathToMove) + print(" Size: " + sizeof_fmt(os.path.getsize(filePathToMove))) + print("To:") + print(" " + leastFull[0]) + print(" Available: " + sizeof_fmt(getFreeSpaceForDir(leastFull[0]))) + + # Move file + if cmdargs.checkonly: + print("------------------------------------------------") + print("Check Only option was used. No files were moved.") + shouldQuit = True + else: + print("------------------------------------------------") + print("Checking System Status...") + if subprocess.call(["/usr/bin/python2", "/usr/LH/bin/idle.py", "-s"]): + print(" System is busy. The file will not be moved.") + sys.exit() + print("Moving File...") + try: + shutil.move(filePathToMove, leastFull[0]) + # eg. src and dest are the same file + except shutil.Error as e: + print(('Error: %s' % e)) + except IOError as e: + print(('Error: %s' % e.strerror)) + + # Remove png files + print("------------------------------------------------") + print("Removing png Files:") + pngFiles = glob.glob(filePathToMove + "*.png") + for p in pngFiles: + os.remove(p) + print(" " + p) diff --git a/linhes/linhes-system/be_check.py b/linhes/linhes-system/be_check.py new file mode 100755 index 0000000..b5f7a64 --- /dev/null +++ b/linhes/linhes-system/be_check.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +#simple program to check if mythbackend is up and running +#exit code of 0 is success, anything else means it can't connect +import sys +class Logger(object): + def __init__(self, filename="/tmp/Default.log"): + #self.terminal = sys.stdout + try: + self.log = open(filename, "a") + except: + pass + + def write(self, message): + try: + #self.terminal.write(message) + self.log.write(message) + except: + pass + +sys.stdout = Logger("/tmp/be_check.log") + + +from MythTV import MythBE,MythDB +#import datetime,time,sys,subprocess + + + + + +try: + be=MythBE() + db = MythDB() +except: + sys.exit(1) +sys.exit(0) diff --git a/linhes/linhes-system/checkXFSfrag.sh b/linhes/linhes-system/checkXFSfrag.sh new file mode 100755 index 0000000..d25eaa8 --- /dev/null +++ b/linhes/linhes-system/checkXFSfrag.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# +# Bash script by Gene Alexander (http://www.eracc.com/contact) +# of ERA Computers & Consulting (www.eracc.com, blog.eracc.com, shopping.eracc.com) +# Written using vim, the BEST plain text file editor in all of Creation. +# +# Teach yourself bash scripting: http://tldp.org/LDP/abs/html/index.html +# +# Purpose: To check fragmentation on XFS with xfs_db and run xfs_fsr on XFS mount points that +# are above a specific fragmentation threshold. +# +# What is xfs_db? Use 'man xfs_db' to find out. +# What is xfs_fsr? Use 'man xfs_fsr' to find out. +# +# Any busy files, such as open logs on /var/log, will be skipped. To defragment logs one should +# wrap this script with another script to stop and restart logging. Or, even better, write +# one's own script just for defragmentation of the logs. +# +# Warranty: NONE. Use at your own discretion and be aware that data loss is on your head if +# you choose to use this script. +# +# License: GPL 2.0 http://www.gnu.org/licenses/gpl-2.0.html +# +# Suggested Usage: crontab file for root +# 0 0 * * * /root/bin/chkxfsfrag # Run at midnight +# +# Original Release: 2011 December 15 (Merry Christmas!) +# DO NOT ALTER HEADER FROM THIS LINE UP. +# +e='/usr/bin/echo -e' # Use the echo command, not built-in. +xfsfsr=/usr/bin/xfs_fsr # Set variable with the path to xfs_fsr. +xfsdb=/usr/bin/xfs_db # Set variable with the path to xfs_db. +ionice=/usr/bin/ionice # Set variable with the path to ionice. +idle='/usr/LH/bin/idle.py -s' # Set varialbe with path to idle.py. +pctmax=12 # Set maxiumum frag percent needed for defrag. + # This is zero here for testing purposes only + # a higher number should be used in production. +array=`df -T|grep xfs|cut -f 1 --delim=" "` # Array of all XFS file systems. +for i in ${array[@]}; +do + #check for idle flag + if [[ $1 == "--idle" ]] + then + while ! $idle + do + echo "System is busy. Waiting 10 minutes before trying again." + sleep 600 + done + fi + #check that the device is SATA and skip defrag on SSDs + device=`echo ${i} | cut -f 3 --delim="/" | sed 's/[0-9]//g'` + isSATA=`cat /sys/block/${device}/queue/rotational` + if [[ $isSATA -eq 1 ]] + then + percentage=`$xfsdb -c frag -r ${i}|grep factor|cut -f 7 --delim=" "` + percent2=`$e $percentage|cut -f 1 --delim=.` + if [ "$percent2" -gt "$pctmax" ] + then + $e "${i} is $percentage fragmented. Running defragment on ${i}." + # Only uncomment one of the following two lines. + #$xfsfsr -v ${i} # Uncomment for verbose defrag. + $ionice -c3 $xfsfsr ${i} # Uncomment for quiet defrag. + else + $e "${i} is $percent2% fragmented and is below the fragmentation threshold of $pctmax%. Skipping." + fi + else + echo "${i} is an SSD. Skipping." + fi +done +exit 0 diff --git a/linhes/linhes-system/create_media_dirs.sh b/linhes/linhes-system/create_media_dirs.sh new file mode 100755 index 0000000..0aa44f5 --- /dev/null +++ b/linhes/linhes-system/create_media_dirs.sh @@ -0,0 +1,74 @@ +#!/bin/bash +#script to create the media directories +#used for building ISO (go.sh) and LiveCD (pre_install.sh) +#call it like so +#create_media_dirs.sh /top_level_path + +topdir=$1 + +if [ x$topdir = "x" ] +then + echo "Top level dir is empty" + exit 1 +fi + +if [ ! -d "$topdir" ] +then + echo "$topdir is not a dir or does not exist" + exit 2 +fi + + + +while read dirname +do + mkdir -p "${topdir}/${dirname}" + touch "${topdir}/${dirname}/.media" + chown mythtv:users "${topdir}/${dirname}" + chmod 775 "${topdir}/${dirname}" + chmod 775 "${topdir}/${dirname}/.media" +done < len(SGs) or SGselect < 1: + SGselect=int("e") + except ValueError: + print("You must enter a number between 1 and " + str(len(SGs)) + ". Exiting.") + sys.exit(0) + + SGgrp=SGs[SGselect-1] + + # Get Storage Group directories + for record in recs: + if record.groupname == SGgrp: + dirname = record.dirname + SGDIRS.append(dirname) + + # If there are less than 2 directories defined bail as we can't move anything + if len(SGDIRS) < 2: + print("There are less than 2 directories defined. Exiting.") + sys.exit(0) + + while not shouldQuit: + SGDIRSdata = [] + print("\n------------------------------------------------") + print("'" + SGgrp + "' Storage Group Directories - Percent Used:") + SGcnt=0 + # Get percent free and size free + for directory in SGDIRS: + # Check if SG path exists + if not os.path.exists(directory): + print(" " + directory + " - Not Mounted") + continue + # Check if SG has data files to move + if len(glob.glob1(directory,"*.ts")) or len(glob.glob1(directory,"*.mpg")) or len(glob.glob1(directory,"*.nuv")) or len(glob.glob1(directory,"*.jpg")): + freePcent = getFreePercentForDir(directory) + freeSize = getFreeSpaceForDir(directory) + SGDIRSdata.append([directory, freePcent, freeSize]) + SGcnt=SGcnt+1 + print("%s: %s - %.2f%%" % (SGcnt, directory, freePcent * 100)) + else: + # Check if the selected SG dir has no data files exit + if SGselectdata and SGselectdata[0] == directory: + print("\n'" + SGgrp + "' Storage Group directories have no files to move. Exiting") + sys.exit(0) + + # Exit if no SGs with data found + if SGcnt is 0: + print("\n'" + SGgrp + "' Storage Group directories have no files to move. Exiting.") + sys.exit(0) + + # Ask user to select which SG to empty if not already selected + if not SGselectdata: + try: + SGDIRselect=int(input("\nEnter the number of the storage group directory to empty: ")) + if SGDIRselect > SGcnt or SGDIRselect < 1: + SGDIRselect=int("e") + except ValueError: + print("You must enter a number between 1 and %s. Exiting." %SGcnt) + sys.exit(0) + + SGselectdata=SGDIRSdata[SGDIRselect-1] + + # Sort data on percent free + SGDIRSdata = sorted(SGDIRSdata, reverse=True, key=operator.itemgetter(1)) + leastFull = SGDIRSdata[-1] + + # Make sure leastFull and SGselectdata are not the same dir + if leastFull[0] == SGselectdata[0]: + leastFull = SGDIRSdata[-2] + + # Get random file from user selected dir + fileToMove = random.choice([f for f in os.listdir(SGselectdata[0]) if f.endswith(".ts") or f.endswith(".mpg") or f.endswith(".nuv") or f.endswith(".jpg")]) + filePathToMove = SGselectdata[0] + "/" + fileToMove + + # Check that the file isn't too big for least used dir + fileSize = getFileSize(filePathToMove) + if (fileSize > getFreeSpaceForDir(leastFull[0])): + # Too big to move + print(filePathToMove + " is too big to move to " + leastFull[0]) + sys.exit() + + print("------------------------------------------------") + print("Move File:") + print(" " + filePathToMove) + print(" Size: " + sizeof_fmt(os.path.getsize(filePathToMove))) + print("To:") + print(" " + leastFull[0]) + print(" Available: " + sizeof_fmt(getFreeSpaceForDir(leastFull[0]))) + + # Move file + if cmdargs.checkonly: + print("------------------------------------------------") + print("Check Only option was used. No files were moved.") + shouldQuit = True + else: + print("------------------------------------------------") + print("Checking System Status...") + if subprocess.call(["/usr/bin/python2", "/usr/LH/bin/idle.py", "-s"]): + print(" System is busy. The file will not be moved.") + sys.exit() + print("Moving File...") + try: + shutil.move(filePathToMove, leastFull[0]) + # eg. src and dest are the same file + except shutil.Error as e: + a=input("\n%s. Overwrite destination (y/n)? " % e) + if a == "y" or a == "Y": + os.remove(leastFull[0] + "/" + fileToMove) + shutil.move(filePathToMove, leastFull[0]) + else: + b=input("\nRemove %s (y/n)? " % filePathToMove) + if b == "y" or b == "Y": + os.remove(filePathToMove) + # eg. source or destination doesn't exist + except IOError as e: + print(('Error: %s' % e.strerror)) + + # Remove png files for Default & LiveTV SGs + if SGgrp == "Default" or SGgrp == "LiveTV": + print("------------------------------------------------") + print("Removing png Files:") + pngFiles = glob.glob(filePathToMove + "*.png") + for p in pngFiles: + os.remove(p) + print(" " + p) diff --git a/linhes/linhes-system/enableIRWake.sh b/linhes/linhes-system/enableIRWake.sh new file mode 100755 index 0000000..8a9847a --- /dev/null +++ b/linhes/linhes-system/enableIRWake.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +for vendProdID in `lsusb | sed -e 's/.*ID \([a-f0-9]\+:[a-f0-9]\+\).*/\1/g'` +do + foundRemote=`grep -i "$vendProdID" /usr/share/linhes/templates/remotes/receiver_usb.id` + if [[ $? = 0 ]] + then + #echo Found: $foundRemote + vendID=`echo $vendProdID | cut -d":" -f1` + prodID=`echo $vendProdID | cut -d":" -f2` + for usbDevice in `grep . /sys/bus/usb/devices/*/power/wakeup | cut -d"/" -f6` + do + foundVendID=`cat /sys/bus/usb/devices/$usbDevice/idVendor` + foundProdID=`cat /sys/bus/usb/devices/$usbDevice/idProduct` + if [[ $foundVendID == $vendID && $foundProdID == $prodID ]] + then + echo "Enable wake for $foundRemote on $usbDevice" + sudo sh -c "echo 'enabled' > /sys/bus/usb/devices/$usbDevice/power/wakeup" + fi + done + fi +done diff --git a/linhes/linhes-system/find_orphans.py b/linhes/linhes-system/find_orphans.py new file mode 100755 index 0000000..964d6e4 --- /dev/null +++ b/linhes/linhes-system/find_orphans.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python2 + +from MythTV import MythDB, MythBE, Recorded, MythError +from socket import timeout + +import os +import sys + +def human_size(s): + s = float(s) + o = 0 + while s > 1000: + s /= 1000 + o += 1 + return str(round(s,1))+('B ','KB','MB','GB','TB')[o] + +class File( str ): + def __new__(self, host, group, path, name, size): + return str.__new__(self, name) + def __init__(self, host, group, path, name, size): + self.host = host + self.group = group + self.path = path + self.size = int(size) + def pprint(self): + name = '%s: %s' % (self.host, os.path.join(self.path, self)) + print(' {0:<90}{1:>8}'.format(name, human_size(self.size)).encode('utf-8')) + def delete(self): + be = MythBE(self.host, db=DB) + be.deleteFile(self, self.group) + +class MyRecorded( Recorded ): + _table = 'recorded' + def pprint(self): + name = '{0.hostname}: {0.title}'.format(self) + if self.subtitle: + name += ' - '+self.subtitle + print(' {0:<70}{1:>28}'.format(name,self.basename).encode('utf-8')) + +def printrecs(title, recs): + print(title) + for rec in sorted(recs, key=lambda x: x.title): + rec.pprint() + print('{0:>87}{1:>12}'.format('Count:',len(recs)).encode('utf-8')) + +def printfiles(title, files): + print(title) + for f in sorted(files, key=lambda x: x.path): + f.pprint() + size = sum([f.size for f in files]) + print('{0:>87}{1:>12}'.format('Total:',human_size(size)).encode('utf-8')) + +def populate(host=None): + unfiltered = [] + kwargs = {'livetv':True} + if host: + with DB as c: + c.execute("""SELECT count(1) FROM settings + WHERE hostname=%s AND value=%s""", + (host, 'BackendServerIP')) + if c.fetchone()[0] == 0: + raise Exception('Invalid hostname specified on command line.') + hosts = [host] + kwargs['hostname'] = host + else: + with DB as c: + c.execute("""SELECT hostname FROM settings + WHERE value='BackendServerIP'""") + hosts = [r[0] for r in c.fetchall()] + for host in hosts: + for sg in DB.getStorageGroup(): + if sg.groupname in ('Videos','Banners','Coverart','Fanart',\ + 'Music','MusicArt', 'Photographs',\ + 'Screenshots','Trailers'): + continue + try: + dirs,files,sizes = BE.getSGList(host, sg.groupname, sg.dirname) + for f,s in zip(files,sizes): + newfile = File(host, sg.groupname, sg.dirname, f, s) + if newfile not in unfiltered: + unfiltered.append(newfile) + except: + pass + + recs = list(DB.searchRecorded(**kwargs)) + + zerorecs = [] + orphvids = [] + for rec in list(recs): + if rec.basename in unfiltered: + recs.remove(rec) + i = unfiltered.index(rec.basename) + f = unfiltered.pop(i) + if f.size < 1024: + zerorecs.append(rec) + name = rec.basename.rsplit('.',1)[0] + for f in list(unfiltered): + if name in f: + unfiltered.remove(f) + for f in list(unfiltered): + if not (f.endswith('.mpg') or f.endswith('.nuv') or f.endswith('.ts')): + continue + orphvids.append(f) + unfiltered.remove(f) + + orphimgs = [] + for f in list(unfiltered): + if not f.endswith('.png'): + continue + orphimgs.append(f) + unfiltered.remove(f) + + dbbackup = [] + for f in list(unfiltered): + if 'sql' not in f: + continue + dbbackup.append(f) + unfiltered.remove(f) + + return (recs, zerorecs, orphvids, orphimgs, dbbackup, unfiltered) + +def delete_recs(recs): + printrecs('The following recordings will be deleted', recs) + print('Are you sure you want to continue? (yes/no)') + try: + res = input('> ') + while True: + if res == 'yes': + for rec in recs: + rec.delete(True, True) + break + elif res == 'no': + break + else: + res = input("'yes' or 'no' > ") + except MythError: + name = '{0.hostname}: {0.title}'.format(rec) + if rec.subtitle: + name += ' - '+rec.subtitle + print("Warning: Failed to delete '" + name + "'") + except KeyboardInterrupt: + pass + except EOFError: + sys.exit(0) + +def delete_files(files): + printfiles('The following files will be deleted', files) + print('Are you sure you want to continue? (yes/no)') + try: + res = input('> ') + while True: + if res == 'yes': + for f in files: + f.delete() + break + elif res == 'no': + break + else: + res = input("'yes' or 'no' > ") + except KeyboardInterrupt: + pass + except EOFError: + sys.exit(0) + +def main(host=None): + while True: + recs, zerorecs, orphvids, orphimgs, dbbackup, unfiltered = populate(host) + + if len(recs): + printrecs("Recordings with missing files", recs) + if len(zerorecs): + printrecs("Zero byte recordings", zerorecs) + if len(orphvids): + printfiles("Orphaned video files", orphvids) + if len(orphimgs): + printfiles("Orphaned snapshots", orphimgs) + if len(dbbackup): + printfiles("Database backups", dbbackup) + if len(unfiltered): + printfiles("Other files", unfiltered) + if not printOnly: + opts = [] + if len(recs): + opts.append(['Delete orphaned recording entries', delete_recs, recs]) + if len(zerorecs): + opts.append(['Delete zero byte recordings', delete_recs, zerorecs]) + if len(orphvids): + opts.append(['Delete orphaned video files', delete_files, orphvids]) + if len(orphimgs): + opts.append(['Delete orphaned snapshots', delete_files, orphimgs]) + if len(unfiltered): + opts.append(['Delete other files', delete_files, unfiltered]) + opts.append(['Refresh list', None, None]) + print('Please select from the following:') + for i, opt in enumerate(opts): + if opt[0] == "Refresh list": + print(' R. {1}'.format(i+1, opt[0])) + refreshNum=i+1 + else: + print(' {0}. {1}'.format(i+1, opt[0])) + + print(' Q. Quit') + try: + inner = True + res = input('> ') + while inner: + try: + res = int(res) + except: + if res == "Q" or res == "q": + sys.exit(0) + elif res == "R" or res == "r": + res = refreshNum + else: + res = input('Invalid selection > ') + continue + if (res <= 0) or (res > len(opts)): + res = input('Invalid selection > ') + continue + break + opt = opts[res-1] + if opt[1] is None: + continue + else: + opt[1](opt[2]) + + except KeyboardInterrupt: + break + except EOFError: + sys.exit(0) + else: + sys.exit(0) +DB = MythDB() +BE = MythBE(db=DB) +DB.searchRecorded.handler = MyRecorded +DB.searchRecorded.dbclass = MyRecorded + +if __name__ == '__main__': + global printOnly + if "--printonly" in sys.argv : + printOnly=True + else: + printOnly=False + + + if len(sys.argv) == 2 and sys.argv[1] != "--printonly": + main(sys.argv[1]) + else: + main() diff --git a/linhes/linhes-system/fstrim.hook b/linhes/linhes-system/fstrim.hook new file mode 100644 index 0000000..3f80657 --- /dev/null +++ b/linhes/linhes-system/fstrim.hook @@ -0,0 +1,9 @@ +[Trigger] +Operation = Install +Type = Package +Target = linhes-system + +[Action] +Description = Enable fstrim.timer... +When = PostTransaction +Exec = /usr/bin/systemctl enable fstrim.timer diff --git a/linhes/linhes-system/idle.py b/linhes/linhes-system/idle.py new file mode 100755 index 0000000..373ae24 --- /dev/null +++ b/linhes/linhes-system/idle.py @@ -0,0 +1,375 @@ +#!/usr/bin/python + +import argparse, os, re, subprocess, sys, time +from datetime import datetime, date, timedelta + +def msg(cmdargs,msg): + if cmdargs.silent is False: + print("%s" %msg) + +def mythshutdownlock_check(cmdargs,cursor): + if (cmdargs.lock): + msg(cmdargs," Checking mythshutdown for lock...") + try: + cursor.execute("select data from settings where value = 'MythShutdownLock'") + results=cursor.fetchone() + except: + return True + lock=results[0] + if int(lock) == 0 : + msg(cmdargs," mythshutdown is NOT locked.") + return True + else: + msg(cmdargs," mythshutdown is locked.") + return False + else: + return True + +def dailywake_check(cmdargs,cursor): + if (cmdargs.daily): + msg(cmdargs," Checking if in a daily wake period...") + dailyWake=False + today = date.today() + now = datetime.now() + try: + cursor.execute("select data from settings where value = 'DailyWakeupStartPeriod1'") + results=cursor.fetchone() + p1Start=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M") + cursor.execute("select data from settings where value = 'DailyWakeupEndPeriod1'") + results=cursor.fetchone() + p1End=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M") + cursor.execute("select data from settings where value = 'DailyWakeupStartPeriod2'") + results=cursor.fetchone() + p2Start=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M") + cursor.execute("select data from settings where value = 'DailyWakeupEndPeriod2'") + results=cursor.fetchone() + p2End=datetime.strptime(' '.join([str(today), results[0]]), "%Y-%m-%d %H:%M") + except: + print("error") + return True + + # Check for time periods that cross midnight + if (p1End < p1Start): + if (now > p1End): + p1End = p1End + timedelta(days=1) + else: + p1Start = p1Start + timedelta(days=-1) + if (p2End < p2Start): + if (now > p2End): + p2End = p2End + timedelta(days=1) + else: + p2Start = p2Start + timedelta(days=-1) + + #Check for one of the daily wakeup periods + if (p1Start != p1End): + if (now >= p1Start and now <= p1End): + msg(cmdargs," Currently in daily wake period 1.") + return False + if (p2Start != p2End): + if (now >= p2Start and now <= p2End): + msg(cmdargs," Currently in daily wake period 2.") + return False + + #Are we about to start a daily wakeup period using the -t TIME var + if (p1Start != p1End): + delta=p1Start-now + if (delta.seconds >= 0 and delta.seconds <= cmdargs.time * 60): + msg(cmdargs," Daily wake period 1 will start in less than %s minutes." %cmdargs.time) + return False + if (p2Start != p2End): + delta=p2Start-now + if (delta.seconds >= 0 and delta.seconds <= cmdargs.time * 60): + msg(cmdargs," Daily wake period 2 will start in less than %s minutes." %cmdargs.time) + return False + + msg(cmdargs," Currently NOT in a daily wake period.") + return True + else: + return True + +def schemalock_check(cmdargs,cursor): + msg(cmdargs," Checking if the schema is locked...") + try: + cursor.execute("select count(*) from schemalock") + results=cursor.fetchone() + except: + return True + schemalock=results[0] + if schemalock == 0: + msg(cmdargs," The schema is NOT locked.") + return True + else: + msg(cmdargs," The schema is locked.") + return False + +def in_use(cmdargs,cursor): + msg(cmdargs," Checking if programs are in use...") + try: + cursor.execute("select count(*) from inuseprograms") + results=cursor.fetchone() + except: + return True + prginuse=results[0] + if prginuse == 0 : + msg(cmdargs," Programs are NOT in use.") + return True + else: + msg(cmdargs," Programs are in use.") + return False + +def job_check(cmdargs,cursor): + msg(cmdargs," Checking jobqueue for active jobs...") + try: + cursor.execute("select count(*) from jobqueue where status between 2 and 5") + results=cursor.fetchone() + except: + return True + jobs=results[0] + if jobs == 0 : + msg(cmdargs," No jobs are active.") + return True + else: + msg(cmdargs," Jobs are active.") + return False + +def upcoming_check(cmdargs,mythBE): + msg(cmdargs," Checking for recordings in the next %s minutes..." %cmdargs.time) + try: + upcoming = mythBE.getUpcomingRecordings() + except: + msg(cmdargs," Could not get upcoming recordings.") + return True + time_diff=10000 + r=0 + for i in upcoming: + r += 1 + if r > 1: + break + show=str(i) + show=show.strip() + showtime=re.split("[-+]\d\d:\d\d",str(i.starttime))[0] + now=time.time() + rec_time=time.strptime( showtime ,"%Y-%m-%d %H:%M:%S" ) + r=time.mktime(rec_time) + time_diff = ( r - now ) / 60 + + if ( time_diff > cmdargs.time) : + msg(cmdargs," No recordings starting in %s minutes." %cmdargs.time) + return True + else: + msg(cmdargs," A recording is starting in %s minutes." %int(time_diff)) + return False + +def mfd_check(cmdargs): + msg(cmdargs," Checking if mythfilldatabase is running...") + with open(os.devnull, "w") as fnull: + mythfilldatabase_ret = subprocess.call(["pidof", "mythfilldatabase"], stdout=fnull) + if mythfilldatabase_ret == 0 : + msg(cmdargs," mythfilldatabase is running.") + return False + else: + msg(cmdargs," mythfilldatabase is NOT running.") + return True + +def mythtvsetup_check(cmdargs): + msg(cmdargs," Checking if mythtv-setup is running...") + with open(os.devnull, "w") as fnull: + mythsetup_ret = subprocess.call(["pidof", "mythtv-setup"], stdout=fnull) + if mythsetup_ret == 0 : + msg(cmdargs," mythtv-setup is running.") + return False + else: + msg(cmdargs," mythtv-setup is NOT running.") + return True + +def userlogins_check(cmdargs): + if (cmdargs.logins): + u=False + msg(cmdargs," Checking for users logged in...") + users=subprocess.check_output("who") + names=([x.split() for x in users.splitlines()]) + for i in names: + if (i[0] == "mythtv" and i[4] == "(:0)"): + msg(cmdargs," Ignoring %s %s" %(i[0],i[4])) + else: + msg(cmdargs," User logged in: %s %s" %(i[0],i[4])) + u=True + if u: + return False + else: + return True + else: + return True + +def sambafiles_check(cmdargs): + if (cmdargs.sambafiles): + msg(cmdargs," Checking if Samba files are in use...") + try: + smbstatus=subprocess.check_output(["smbstatus", "-L"]) + except: + smbstatus="No locked files" + if "No locked files" in smbstatus: + msg(cmdargs," Samba files are NOT in use.") + return True + else: + msg(cmdargs," Samba files are in use.") + return False + else: + return True + +def mythfe_check(cmdargs,cursor,mythDB): + #checks to see if a frontend is considered idle + # True means FE is idle + + if ( cmdargs.runningfe ): + msg(cmdargs," Checking for running and playing mythfrontends...") + else: + msg(cmdargs," Checking for playing mythfrontends...") + try: + cursor.execute("select distinct hostname from settings where hostname is not null;") + frontends=cursor.fetchall() + except: + return True + + for i in frontends: + try: + msg(cmdargs," Checking %s's mythfrontend status..." %i) + frontend = mythDB.getFrontend(''.join(i)) + if ( cmdargs.runningfe ): + msg(cmdargs," %s's mythfrontend is RUNNING." %i) + return False + location = frontend.sendQuery('Location') + + if location == "standbymode": + msg(cmdargs," %s's mythfrontend is in Standby Mode." %i) + continue + + if ( location.startswith('Playback ') ): + msg(cmdargs," %s's mythfrontend is PLAYING." %i) + return False + else: + msg(cmdargs," %s's mythfrontend is NOT playing." %i) + + if '.xml' in location or 'mainmenu' in location: + msg(cmdargs," %s's mythfrontend is in MENUS." %i) + else: + #FE is not in menus, so it must be active in a plugin + msg(cmdargs," %s's mythfrontend is NOT in menus." %i) + return False + except: + msg(cmdargs," Could not connect to %s's mythfrontend." %i) + + if ( cmdargs.runningfe ): + msg(cmdargs," mythfrontends are not running or playing or are in menus.") + else: + msg(cmdargs," mythfrontends are not playing or are in menus.") + + return True + +def usage(): + line = ''' + idle.py checks if the system is idle. + Use idle.py -h to see options. + + idle.py checks these parts of the system in this order to + determine if it is idle: + - (option -g) users are logged in return busy + ignores mythtv (:0) for busy + - (option -f) Samba files are in use return busy + - (option -l) mythshutdown is locked return busy + - (option -d) in a daily wake period or + about to start a daily wake period return busy + checks the next 15 minutes. -t TIME changes time + - schema is locked return busy + - there are in use programs return busy + - there are active jobs in the job queue return busy + - mythfilldatabase is running return busy + - mythtv-setup is running return busy + - there are upcoming recordings return busy + checks the next 15 minutes. -t TIME changes time + - (option -r) mythfrontends running return busy + - mythfrontends playing back a recording or video return busy + - mythfrontends not in menus return busy + + idle.py stops checking and returns false (busy) when the first busy is found. + ''' + print(line) + sys.exit(0) + +def main(args=[False]): + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--daily', action='store_true', help='Include daily wake & about to start wake in system busy. (default: daily wake & about to start wake is system idle)') + parser.add_argument('-g', '--logins', action='store_true', help='Include user logins in system busy. Ignores mythtv (:0) in system busy.') + parser.add_argument('-f', '--sambafiles', action='store_true', help='Include Samba files in use in system busy.') + parser.add_argument('-l', '--lock', action='store_true', help='Include mythshutdown lock in system busy. (default: mythshutdown lock is system idle)') + parser.add_argument('-r', '--runningfe', action='store_true', help='Include running mythfrontends in system busy. (default: running mythfrontends are system idle)') + parser.add_argument('-s', '--silent', action='store_true', help='Run without printing output. Recommended for use in cron jobs or scripts.') + parser.add_argument('-t', '--time', type=int, default=15, help='Minutes of idle time needed to return idle for upcoming recordings and daily wake.') + parser.add_argument('-u', '--usage', action='store_true', help='Print usage instructions.') + if args[0] is False: + cmdargs = parser.parse_args() + else: + cmdargs = parser.parse_args(args) + + if cmdargs.usage: + usage() + idle=True + msg(cmdargs,"Checking system idle...") + + if (userlogins_check(cmdargs)): + idle = True + else: + idle = False + + if (idle and sambafiles_check(cmdargs)): + idle = True + else: + idle = False + + try: + from MythTV import MythDB + mythDB = MythDB() + cursor = mythDB.cursor() + db_conn=True + except: + msg(cmdargs,"Couldn't connect to MythTV database.") + db_conn=False + + try: + from MythTV import MythBE + mythBE = MythBE() + be_conn=True + except: + msg(cmdargs,"Couldn't connect to MythTV backend.") + be_conn=False + + if ( db_conn and idle ): + if (mythshutdownlock_check(cmdargs,cursor) and dailywake_check(cmdargs,cursor) and schemalock_check(cmdargs,cursor) and in_use(cmdargs,cursor) and job_check(cmdargs,cursor)): + idle=True + else: + idle=False + + if ( be_conn and idle ): + if (mfd_check(cmdargs) and mythtvsetup_check(cmdargs) and upcoming_check(cmdargs,mythBE)): + idle=True + else: + idle=False + + if ( db_conn and idle ): + if (mythfe_check(cmdargs,cursor,mythDB)): + idle=True + else: + idle=False + + if ( idle ): + msg(cmdargs,"System is idle.") + else: + msg(cmdargs,"System is busy.") + return idle + +if __name__ == "__main__": + idle=main() + if ( idle ): + exit(0) + else: + exit(1) diff --git a/linhes/linhes-system/jobqueue_helper.py b/linhes/linhes-system/jobqueue_helper.py new file mode 100755 index 0000000..6a567cb --- /dev/null +++ b/linhes/linhes-system/jobqueue_helper.py @@ -0,0 +1,63 @@ +#!/usr/bin/python + +import argparse, os, re, subprocess, sys, time +from MythTV import MythDB, Job + +mythDB = MythDB() +cursor = mythDB.cursor() + +def set_cmds(cmdargs,job): + #print "Setting cmds on job %s to %s" %(cmdargs.jobid,cmdargs.cmd) + cursor.execute("update jobqueue set cmds = '%s' where id = '%s'" %(cmdargs.cmd,cmdargs.jobid)) + +def set_comment(cmdargs,job): + #print "Setting comment on job %s to %s" %(cmdargs.jobid,cmdargs.comment) + job.setComment("%s" %cmdargs.comment) + +def set_status(cmdargs,job): + #print "Setting status on job %s to %s" %(cmdargs.jobid,cmdargs.status) + job.setStatus("%s" %cmdargs.status) + +def run_cursor(cmdargs): + cursor.execute("%s" %cmdargs.man_cursor) + results=cursor.fetchone() + print(results[0]) + +def usage(): + line = ''' + jobqueue_helper.py provides MythTV job queue functions + using python bindings for bash scripts. + Use jobqueue_helper.py -h to see options. + ''' + print(line) + sys.exit(0) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('-cs', '--comment_set', action='store', dest='comment', help='Set the comment of the jobid') + parser.add_argument('-cmds', '--cmd_set', type=int, default=77777, action='store', dest='cmd', help='Set the cmd of the jobid') + parser.add_argument('-ss', '--status_set', type=int, action='store', dest='status', help='Set the status of the jobid') + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument('-j', '--jobid', type=int, help='jobid of the job to control') + action.add_argument('-m', '--man_cursor', action='store', dest='man_cursor', help='Manual mysql cursor command') + action.add_argument('-u', '--usage', action='store_true', help='Print usage instructions.') + + cmdargs = parser.parse_args() + + if cmdargs.usage: + usage() + + if cmdargs.jobid: + job = Job(cmdargs.jobid) + + if cmdargs.comment: + set_comment(cmdargs, job) + + if cmdargs.status: + set_status(cmdargs, job) + + if cmdargs.cmd != 77777: + set_cmds(cmdargs, job) + + if cmdargs.man_cursor: + run_cursor(cmdargs) diff --git a/linhes/linhes-system/lh_systemstart.sh b/linhes/linhes-system/lh_systemstart.sh new file mode 100755 index 0000000..cd32cdf --- /dev/null +++ b/linhes/linhes-system/lh_systemstart.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +. /etc/profile +#. /etc/systemconfig + +function msg(){ + /usr/bin/notify-send --app-name="LinHES" --icon=dialog-information "$1" "$2" +} + +function applyUIsettings(){ + plasma-apply-wallpaperimage /usr/share/linhes/templates/lights-bud-abstract-4k-cq.jpg & + plasma-apply-colorscheme BreezeDark & + kwriteconfig5 --group KDE --key SingleClick false + kwriteconfig5 --file ~/.config/kscreenlockerrc --group Daemon --key Autolock false + kwriteconfig5 --file ~/.config/kscreenlockerrc --group Daemon --key LockOnResume false + cp /usr/share/linhes/templates/plasma-org.kde.plasma.desktop-appletsrc ~/.config/ + #restart plasma + kquitapp5 plasmashell + kstart5 plasmashell + #disable file indexing + balooctl disable & +} + + + +#-------MAIN------- +/usr/bin/enableIRWake.sh & +msg "Welcome to LinHES 9!" +applyUIsettings diff --git a/linhes/linhes-system/lh_systemstart.sh.desktop b/linhes/linhes-system/lh_systemstart.sh.desktop new file mode 100644 index 0000000..478f3ab --- /dev/null +++ b/linhes/linhes-system/lh_systemstart.sh.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Exec=/usr/bin/lh_systemstart.sh +Icon=dialog-scripts +Name=lh_systemstart.sh +Path= +Type=Application +X-KDE-AutostartScript=true diff --git a/linhes/linhes-system/myth2mkv b/linhes/linhes-system/myth2mkv new file mode 100755 index 0000000..51a0c9b --- /dev/null +++ b/linhes/linhes-system/myth2mkv @@ -0,0 +1,466 @@ +#!/bin/bash +# +# Convert video to AVC-1 / h264 +# +# version 0.27-001 +# +# Prerequisites: +# - mythtv >= 0.27 +# - handbrake-cli +# - mplayer +# - mkvtoolnix +# - jobqueue_helper.py +# +# Arguments +# $1 must be the directory/file of the recording +# $2 must be chanid +# $3 must be starttime +# $4 must be title +# $5 must be subtitle +# $6 must be jobid +# $7 must be quality of encode +# +# As a MythTV user job: +# myth2mkv "%DIR%/%FILE%" "%CHANID%" "%STARTTIME%" "%TITLE%" "%SUBTITLE%" "%JOBID%" "HP|HQ|MQ|LQ" +# Select only 1 quality setting +# HP is similar to the HandBrake built-in preset High Profile + +######################## +# # +# Adjustable variables # +# # +######################## + +OUTDIR=/myth/video +LOGPATH=/var/log/mythtv +LOGFILE=${LOGPATH}/myth2mkv-$$.log + +# TMPDIR is for large transient files +TMPDIR=/myth/tmp + +# x264 tuning: +# Tune x264 based on content. Valid options for TUNING are: +# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency +# Separate multiple options with a comma. DEFAULT: none +TUNING="" + +# Custom cropping. Useful if you have a 4:3 image in a HD frame or if +# HandBrake's autocrop smarts fail you. +# Crop 240 pixels off the left and right for 4:3 image in 1920x1080 frame +# Crop 160 pixels off the left and right for 4:3 image in 1280x720 frame +# +# i.e. 0:0:240:240 +# Default: In HP and HQ: CROP="0:0:0:0" (no cropping). +# IN MQ and LQ: autocrop. +CROP="" + +# Force custom output resolution. +# Default: Keep same resolution as input file (less any cropping). +# The HP quality setting always keeps the same resolution as the input file. +WIDTH="" +HEIGHT="" + +# Force use/non-use of deinterlacing filter. Y|N|G (Yes, No, Guess) +# Default: G - Guess based on source resolution. +# If the source video width is 1920, 1440, 852, 704, 640 or 528 pixels +# "G" will deinterlace the video. Change to "Y" to force use of deinterlacing +# filter or to "N" to NOT use deinterlace filter no matter the resolution. +DEINT="G" + +############################ +# # +# End adjustable variables # +# # +############################ + +if [[ ! -d ${TMPDIR} ]] ; then + mkdir -p ${TMPDIR} +fi + +if [[ ! -d ${OUTDIR} ]] ; then + mkdir -p ${OUTDIR} +fi + +#------FUNCTIONS--------------- +update_comment() +# Arg_1 = COMMENT +{ +if [ ${NO_JOBID} -eq 0 ]; then + `jobqueue_helper.py -j ${JOBID} -cs "${1}"` +fi +} + +check_background_progress() +# check handbrake progress in background +{ +while [ ! -f ${HB_RETURN_CODE} ] +do + sleep 15 + check_myth_jobcmds + pass=`tail -1 ${STATUSFILE} | egrep -o -e 'task [0-9] of [0-9], ' | tail -1 | sed 's/task\ /Pass\ /g'` + prog_percent=`tail -1 ${STATUSFILE} | egrep -o -e '[0-9]*\.[0-9]. %' | tail -1 | sed 's/\ %//g'` + current_FPS=`tail -1 ${STATUSFILE} | egrep -o -e 'avg [0-9.]*\.[0-9]* fps' | tail -1 | sed -e 's/avg\ //g' -e 's/\ fps//g'` + current_ETA=`tail -1 ${STATUSFILE} | egrep -o -e 'ETA [0-9.][0-9.]h[0-9.][0-9.]m[0-9.][0-9.]s' | tail -1` + if [ -n "$prog_percent" ]; then + echo "${pass}${prog_percent}% @ ${current_FPS} fps. ${current_ETA}" + update_comment "${pass}${prog_percent}% @ ${current_FPS} fps. ${current_ETA}" + fi +done +} + +check_myth_jobcmds() +# check the myth database for stop pause or resume commands +{ +if [[ ${NO_JOBID} -eq 0 ]] ; then + CURRENT_CMD=`jobqueue_helper.py -m "select cmds from jobqueue where id = ${JOBID}"` + case "${CURRENT_CMD}" in + # JOB_RUN + 0) ;; + # JOB_PAUSE + 1) `jobqueue_helper.py -j ${JOBID} -ss 6` + kill -s STOP ${handbrake_pid} ;; + # JOB_RESUME + 2) `jobqueue_helper.py -j ${JOBID} -ss 4` + `jobqueue_helper.py -j ${JOBID} -cmds 0` + kill -s CONT ${handbrake_pid} ;; + # JOB_STOP + 4) `jobqueue_helper.py -j ${JOBID} -ss 5` + `jobqueue_helper.py -j ${JOBID} -cmds 0` + kill -9 ${handbrake_pid} ${command_pid} + clean_up_files + echo "Encode Cancelled" >> ${LOGFILE} + `jobqueue_helper.py -j ${JOBID} -ss 320` + exit ;; + esac +fi +} + +get_info_for_hb() { +# Collect some info about source file + +/usr/bin/mplayer -nolirc -identify -frames 0 "${HBINPUTFILE}" \ + 2>/dev/null 1>"${IDFILE}" + +VIDEOW=$( grep ID_VIDEO_WIDTH= "${IDFILE}" | awk -F= '{ print $NF }' ) +FPS=$( grep ID_VIDEO_FPS= "${IDFILE}" | awk -F= '{ print $NF }' ) + +# HandBrake does not like a framerate of 29.970, so let's drop the 0 +if [[ ${FPS} = "29.970" ]] ; then + FPS="29.97" +fi + +# HandBrake does not like a framerate of 59.940, so let's drop the 0 +if [[ ${FPS} = "59.940" ]] ; then + FPS="59.94" +fi + +# A rough guestimation that if the video width is 1920, 1440, 852, 704, 640 or +# 528 pixels it is probably interlaced. +if [[ ${DEINT} = Y ]] ; then + DEINT="-d slow" +else + if [[ ${DEINT} = N ]] ; then + DEINT="" + else + if [[ ${VIDEOW} = 1920 || ${VIDEOW} = 1440 || ${VIDEOW} = 852 || \ + ${VIDEOW} = 704 || ${VIDEOW} = 640 || ${VIDEOW} = 528 ]] ; then + DEINT="-d slow" + else + DEINT="" + fi + fi +fi + +if [[ -n ${DEINT} ]] ; then + if [[ ${QUALITY} = LQ ]] ; then + DEINT="-d fast" + fi +fi + +if [[ -n ${TUNING} ]] ; then + TUNING="--x264-tune ${TUNING}" +fi + +if [[ -n ${CROP} ]] ; then + CROP="--crop ${CROP}" +fi + +if [[ -n ${WIDTH} ]] ; then + WIDTH="-w ${WIDTH} -X ${WIDTH}" +fi + +if [[ -n ${HEIGHT} ]] ; then + HEIGHT="-l ${HEIGHT} -Y ${HEIGHT}" +fi + +if [[ ${QUALITY} = HP ]] ; then + if [[ -n ${CROP} ]] ; then + CROP="--crop ${CROP}" + else + CROP="--crop 0:0:0:0 --auto-anamorphic" + fi + HB_OPTS="-o ${TMPFILE} -e x264 ${TUNING} -q 20.0 -a 1,1 -E copy:ac3,faac -B 160,160 -6 dpl2,auto -R Auto,Auto -D 0.0,0.0 --audio-copy-mask aac,ac3,dtshd,dts,mp3 --audio-fallback ffac3 -f mkv --decomb --loose-anamorphic --modulus 2 -m --x264-preset medium --h264-profile high --h264-level 4.1 ${CROP} -s 1" +elif [[ ${QUALITY} = HQ ]] ; then + if [[ -n ${CROP} ]] ; then + CROP="--crop ${CROP}" + else + CROP="--crop 0:0:0:0 --auto-anamorphic" + fi + HB_OPTS="-o ${TMPFILE} -f mkv -m -e x264 ${TUNING} -x b-adapt=2:rc-lookahead=50 -b 5000 -2 -T ${WIDTH} ${HEIGHT} -r ${FPS} --cfr ${CROP} ${DEINT} -a 1 -E copy -s 1" +else + if [[ ${CROP} = "--crop 0:0:0:0" ]] ; then + CROP="${CROP} --auto-anamorphic" + fi + if [[ ${QUALITY} = LQ ]] ; then + HB_OPTS="-o ${TMPFILE} -f mkv -m -e x264 ${TUNING} -b 1250 ${WIDTH} ${HEIGHT} -r ${FPS} --pfr ${CROP} ${DEINT} -a 1 -E lame -B 128 -Q 8 -6 stereo -s 1" + else + # Fallback to "MQ" + HB_OPTS="-o ${TMPFILE} -f mkv -m -e x264 ${TUNING} -b 2500 -2 -T ${WIDTH} ${HEIGHT} -r ${FPS} --pfr ${CROP} ${DEINT} -a 1 -E lame -B 256 -Q 3 -6 stereo -s 1" + fi +fi +} + +get_handbrake_pid() +{ +process_name="" +i1=1 +while [ "${process_name}" != "found" ]; do + handbrake_pid=`expr ${handbrake_pid} + 1` + i1=`expr ${i1} + 1` + if [ "`ps ${handbrake_pid} | grep HandBrakeCLI | sed 's_.*\(HandBrakeCLI\).*_\1_'`" = "HandBrakeCLI" ]; then + process_name="found" + fi + if [ ${i1} -gt 20 ]; then + break + fi +done +} + +tag_file() { +DATE=`date` + +# Create a tag file here +echo "" > "${TAG_FILE}" +echo "" >> "${TAG_FILE}" +echo "" >> "${TAG_FILE}" +echo "" >> "${TAG_FILE}" +echo "" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " TITLE" >> "${TAG_FILE}" +echo " ${TITLE}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " SUBTITLE" >> "${TAG_FILE}" +echo " ${SUBTITLE}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " SUMMARY" >> "${TAG_FILE}" +echo " ${DESCR}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " DATE_RELEASED" >> "${TAG_FILE}" +echo " ${OAD}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " SEASON" >> "${TAG_FILE}" +echo " ${SEASON}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " EPISODE" >> "${TAG_FILE}" +echo " ${EPISODE}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " ENCODED_BY" >> "${TAG_FILE}" +echo " HandBrakeCLI ${HBCLIVER}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " DATE_TAGGED" >> "${TAG_FILE}" +echo " ${DATE}" >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo " " >> "${TAG_FILE}" +echo "" >> "${TAG_FILE}" + +# Add tag info into MKV file +echo "Adding tag info to ${TITLE} - ${SUBTITLE} ..." >> ${LOGFILE} + +/usr/bin/mkvpropedit -r ${LOGFILE} -t all:"${TAG_FILE}" "${TMPFILE}" +} + +clean_up_files() +# clean up left over files +{ +unlink ${TMPFILE} 2> /dev/null +unlink ${TMPCUTFILE} 2> /dev/null +unlink ${TMPCUTFILE}.map 2> /dev/null +unlink ${STATUSFILE} 2> /dev/null +unlink ${IDFILE} 2> /dev/null +unlink ${HB_RETURN_CODE} 2> /dev/null +unlink ${TAG_FILE} 2> /dev/null +} + +#-------MAIN SCRIPT------------ + +# create temp filename so multiple instances won't conflict +TMPNAME=toX264-$$ +TMPFILE=${TMPDIR}/${TMPNAME}.mkv +TMPCUTFILE=${TMPDIR}/${TMPNAME}.mpg +HBINPUTFILE="${1}" +TITLE="${4}" +SUBTITLE="${5}" +JOBID="${6}" +QUALITY="${7}" +BASE=`basename ${HBINPUTFILE}` +HBCLIVER=`pacman -Q | grep handbrake-cli | awk '{ print $NF }' | awk -F"-" '{ print $1 }'` +STATUSFILE=/tmp/${TMPNAME}-status.log +HB_RETURN_CODE=/tmp/${TMPNAME}-hb_return_code +IDFILE=/tmp/${TMPNAME}-id.txt +TAG_FILE=/tmp/${TMPNAME}.xml +SEASON=`jobqueue_helper.py -m "select season from recorded where basename LIKE '${BASE}'"` +SEASON=`printf "%02d" $SEASON` +EPISODE=`jobqueue_helper.py -m "select episode from recorded where basename LIKE '${BASE}'"` +EPISODE=`printf "%02d" $EPISODE` +OAD=`jobqueue_helper.py -m "select originalairdate from recorded where basename LIKE '${BASE}'"` +DESCR=`jobqueue_helper.py -m "select description from recorded where basename LIKE '${BASE}'" | sed 's/\&/and/g'` +USER=`whoami` + +# check if %JOBID% is passed from command line +if [ -z ${JOBID} ]; then + NO_JOBID=1 +else + NO_JOBID=0 +fi + +# log file location +CDate="`date`" +echo "" >> ${LOGFILE} +echo $CDate >> ${LOGFILE} +echo "File to encode: ${HBINPUTFILE}" >> ${LOGFILE} +echo " --> Name: ${TITLE} - ${SUBTITLE}" >> ${LOGFILE} +echo " --> Temporary Files: ${TMPNAME}.*" >> ${LOGFILE} +echo "" >> ${LOGFILE} + +get_info_for_hb +ERROR=$? + +if [[ ${ERROR} != 0 ]] ; then + echo "Error parsing source file information!" >> ${LOGFILE} + cat ${IDFILE} >> ${LOGFILE} + clean_up_files + exit 1 +fi + +# start timer +beforetime="$(date +%s)" + +check_myth_jobcmds + +# If there is a cutlist, use it: +if [[ -n `mythutil --getcutlist --chanid "${2}" --starttime "${3}" | grep \ + Cutlist: | awk -F": " '{ print $NF }'` ]] ; then + echo "Applying cutlist for ${TITLE} - ${SUBTITLE} ..." >> ${LOGFILE} + mythtranscode --chanid "${2}" --starttime "${3}" -m --honorcutlist \ + -q --loglevel info --logpath "${LOGPATH}" -o "${TMPCUTFILE}" + mythtrans_pid=$! + ERROR=$? + HBINPUTFILE=${TMPCUTFILE} +fi + +if [[ ${ERROR} != 0 ]] ; then + echo "MythTranscode error!" >> ${LOGFILE} + echo "Check ${LOGPATH}/mythtranscode.date.${mythtrans_pid}.log for mythtranscode error" >> ${LOGFILE} + clean_up_files + exit 1 +fi + +# run handbrake in background to do conversion +echo "Encoding ${TITLE} - ${SUBTITLE} ..." >> ${LOGFILE} + +( /usr/bin/nice -n19 nohup /usr/bin/HandBrakeCLI -i ${HBINPUTFILE} ${HB_OPTS} \ + > ${STATUSFILE} 2>&1 ; echo $? > ${HB_RETURN_CODE} ) & +handbrake_pid=$! +command_pid=${handbrake_pid} +get_handbrake_pid + +check_background_progress + +if [[ `cat ${HB_RETURN_CODE}` != 0 ]] ; then + echo "HandBrakeCLI error!" >> ${LOGFILE} + cat ${STATUSFILE} >> ${LOGFILE} + clean_up_files + exit 1 +fi + +tag_file +ERROR=$? + +if [[ ${ERROR} != 0 ]] ; then + echo "Error creating tag file!" >> ${LOGFILE} + cat ${TAG_FILE} >> ${LOGFILE} + clean_up_files + exit 1 +fi + +# make output filename unique and do not clobber an existing file +# Build a final file name +if [[ $SEASON != "00" && $EPISODE != "00" ]]; then + FILE=$( echo "${TITLE,,} s${SEASON}e${EPISODE} ${SUBTITLE,,}" | tr -d [:punct:] | tr [:blank:] "_" | tr -s "_" ) +else + FILE=$( echo "${TITLE,,} ${OAD} ${SUBTITLE,,}" | tr -d [:punct:] | tr [:blank:] "_" | tr -s "_" ) +fi +OUTPUTFILE="${OUTDIR}/${FILE}.mkv" +i=1 +while [ -e "${OUTPUTFILE}" ] +do + OUTPUTFILE="${OUTDIR}/${FILE}-${i}.mkv" + i=`expr $i + 1` +done + +# move temp file to output location +chown -v "${USER}" "${TMPFILE}" >> ${LOGFILE} +mv -v "${TMPFILE}" "$OUTPUTFILE" >> ${LOGFILE} +ERROR=$? + +if [[ ${ERROR} != 0 ]] ; then + echo "Error moving ${TMPFILE} to ${OUTPUTFILE} !" >> ${LOGFILE} + clean_up_files + exit 1 +fi + +# stop timer +aftertime="$(date +%s)" +seconds="$(expr ${aftertime} - ${beforetime})" + +if [ ${ERROR} -eq 0 ]; then + echo "File Encoded Successfully: ${OUTPUTFILE}" >> ${LOGFILE} + hours=$((seconds / 3600)) + seconds=$((seconds % 3600)) + minutes=$((seconds / 60)) + seconds=$((seconds % 60)) + if [ $hours -eq 0 ]; then + hours="" + elif [ $hours -eq 1 ]; then + hours=" $hours hour" + else + hours=" $hours hours" + fi + if [ $minutes -eq 1 ]; then + minutes="$minutes minute" + else + minutes="$minutes minutes" + fi + if [ $seconds -eq 1 ]; then + seconds="$seconds second" + else + seconds="$seconds seconds" + fi + + echo "Encoding took${hours} ${minutes} ${seconds} @ ${current_FPS} fps." >> ${LOGFILE} + `jobqueue_helper.py -j ${JOBID} -ss 272` + update_comment "Encode Successful. Encoding Time:${hours} ${minutes} ${seconds}" +else + echo "ERROR: ${ERROR}" >> ${LOGFILE} +fi + +# Clean up +clean_up_files diff --git a/linhes/linhes-system/myth2mp3 b/linhes/linhes-system/myth2mp3 new file mode 100755 index 0000000..69d5d3f --- /dev/null +++ b/linhes/linhes-system/myth2mp3 @@ -0,0 +1,96 @@ +#!/bin/sh +# convert recordings to as mp3 audio only +# version 1.1.3 + +# usage: +# first parameter must be %DIR%/%FILE% of the recording +# second parameter must be the desired base name of the output +# third parameter must be %CHANID% if you set USECUTLIST=Y +# fourth parameter must be %STARTTIME% if you set USECUTLIST=Y +# In the mythtv setup screen invoke this script like this: +# MYTHTV User Job Command: +# /usr/LH/bin/myth2mp3 "%DIR%/%FILE%" "%TITLE% - %SUBTITLE%" "%CHANID%" "%STARTTIME%" + +# options: +BITRATE=256k #ie. 128k, 160k, 192k, 224k, 256k +USECUTLIST=Y #Y or N + +# where the converted audio is stored +OUT_DIR=/myth/music + +# create temp filename so multiple instances won't conflict +TMPNAME=toMP3-$$ +TMPFILE=/myth/tmp/$TMPNAME +TMPCUTFILE=/myth/tmp/$TMPNAME.mpg +FFINPUTFILE=$1 +TITLE=`echo $2 | sed 's/\//_/g'` + +# log file location +LOGFILE=/var/log/mythtv/myth2mp3.log +CDate="`date`" +echo "" >> $LOGFILE +echo $CDate >> $LOGFILE +echo "File to encode: $1 Name: $TITLE" >> $LOGFILE + +# start timer +beforetime="$(date +%s)" + +# check if using cutlist +if [ $USECUTLIST = Y ];then + MYTHCOMMFRAMES=`mythutil --getcutlist --chanid "$3" --starttime "$4" | grep 'Cutlist:' | cut -d \ -f 2` + if [ -n "$MYTHCOMMFRAMES" ]; then + echo "Extracting Cutlist..." >> $LOGFILE + /usr/bin/nice -n19 /usr/bin/mythtranscode --chanid "$3" --starttime "$4" --outfile "$TMPCUTFILE" --mpeg2 --honorcutlist + FFINPUTFILE=$TMPCUTFILE + fi +fi + +# run ffmpeg to do conversion to wav +echo "Encoding to intermediate wav..." >> $LOGFILE +/usr/bin/nice -n19 /usr/bin/ffmpeg -i "$FFINPUTFILE" -vn -acodec pcm_s16le -ar 44100 -ac 2 "$TMPFILE.wav" +ERROR=$? + +# Normalize the intermediate wav +echo "Normalizing intermediate wav..." >> $LOGFILE +/usr/bin/nice -n19 /usr/bin/normalize -q "$TMPFILE.wav" +ERROR=$? + +FFINPUTFILE=$TMPFILE.wav + +# Final encode of normalized wav to mp3 +echo "Encoding normalized wav to mp3..." >> $LOGFILE +/usr/bin/nice -n19 /usr/bin/ffmpeg -i "$FFINPUTFILE" -vn -acodec libmp3lame -ab $BITRATE -ar 44100 -ac 2 "$TMPFILE.mp3" +ERROR=$? + +# make output filename unique +OUTPUTFILE=$OUT_DIR/$TITLE.mp3 +i=1 +while [ -e "$OUTPUTFILE" ] +do + OUTPUTFILE=$OUT_DIR/$TITLE-$i.mp3 + i=`expr $i + 1` +done + +# move temp file to output location +chown mythtv "$TMPFILE.mp3" && mv "$TMPFILE.mp3" "$OUTPUTFILE" + +# stop timer +aftertime="$(date +%s)" +seconds="$(expr $aftertime - $beforetime)" + +if [ $ERROR -eq 0 ]; then + echo "File Encoded Sucessfully: $OUTPUTFILE" >> $LOGFILE + hours=$((seconds / 3600)) + seconds=$((seconds % 3600)) + minutes=$((seconds / 60)) + seconds=$((seconds % 60)) + echo "Encoding Time: $hours hour(s) $minutes minute(s) $seconds second(s)" >> $LOGFILE +else + echo "ERROR: $ERROR" >> $LOGFILE +fi + +# clean up left over files +unlink $TMPFILE.mp3 2> /dev/null +unlink $TMPFILE.wav 2> /dev/null +unlink $TMPCUTFILE 2> /dev/null +unlink $TMPCUTFILE.map 2> /dev/null diff --git a/linhes/linhes-system/myth_mtc.cron b/linhes/linhes-system/myth_mtc.cron new file mode 100644 index 0000000..6d70058 --- /dev/null +++ b/linhes/linhes-system/myth_mtc.cron @@ -0,0 +1,79 @@ +#!/bin/bash +MYTH_RUN_STATUS=1 +. /etc/systemconfig +. /etc/profile + +date=`date +%Y-%m-%d` +timestamp=`date +'%Y-%m-%d %H:%M'` +hostname=`hostname` +logFile="/var/log/${date}/${hostname}_myth_mtc.log" +log="logger -t myth_mtc -p local6.info" + +if [ ! -f $logFile ]; then + touch $logFile + echo "" | $log +fi + +#check logfile for Finished and if not run myth_mtc.py +if ! grep -q "Finished Maintenance" $logFile +then + + if ! grep -q "Finished checking size of MythTV home" $logFile + then + MYTHCONFDIR=/usr/share/mythtv unbuffer myth_mtc.py --check_home | $log + if [ $? = 0 ] + then + echo "" | $log + else + echo "Time Exceeded" | $log + exit + fi + fi + + if ! grep -q "Finished Optimize" $logFile + then + if [ $SystemType = Frontend_only ] + then + echo "Will not run Optimize on Frontend Only systems." | $log + echo "Finished Optimize" | $log + else + MYTHCONFDIR=/usr/share/mythtv unbuffer myth_mtc.py --optimize | $log + if [ $? = 0 ] + then + echo "" | $log + else + echo "Time Exceeded" | $log + exit + fi + fi + fi + + if ! grep -q "Finished Backup" $logFile && grep -q "Finished Optimize" $logFile + then + MYTHCONFDIR=/usr/share/mythtv unbuffer myth_mtc.py --backup | $log + if [ $? = 0 ] + then + echo "" | $log + else + echo "Time Exceeded" | $log + exit + fi + fi + + if ! grep -q "Finished Update" $logFile + then + MYTHCONFDIR=/usr/share/mythtv unbuffer myth_mtc.py --update | $log + if [ $? = 0 ] + then + echo "" | $log + else + echo "Time Exceeded" | $log + exit + fi + fi + + if grep -q "Finished checking size of MythTV home" $logFile && grep -q "Finished Optimize" $logFile && grep -q "Finished Backup" $logFile && grep -q "Finished Update" $logFile + then + echo "Finished Maintenance" | $log + fi +fi diff --git a/linhes/linhes-system/openssh.hook b/linhes/linhes-system/openssh.hook new file mode 100644 index 0000000..0f9f866 --- /dev/null +++ b/linhes/linhes-system/openssh.hook @@ -0,0 +1,9 @@ +[Trigger] +Operation = Install +Type = Package +Target = linhes-system + +[Action] +Description = Enable and start sshd... +When = PostTransaction +Exec = /usr/bin/systemctl enable --now sshd.service diff --git a/linhes/linhes-system/paccache.cron b/linhes/linhes-system/paccache.cron new file mode 100644 index 0000000..d3ca082 --- /dev/null +++ b/linhes/linhes-system/paccache.cron @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/bin/paccache -r -q diff --git a/linhes/linhes-system/readme_is_xml b/linhes/linhes-system/readme_is_xml new file mode 100644 index 0000000..f0a13cb --- /dev/null +++ b/linhes/linhes-system/readme_is_xml @@ -0,0 +1 @@ +Files ending in .conf will be read by gen_is_xml.py or gen_lib_xml.py or gen_game_xml.py and output xml as appropriate diff --git a/linhes/linhes-system/remove_storage.py b/linhes/linhes-system/remove_storage.py new file mode 100755 index 0000000..15dbffc --- /dev/null +++ b/linhes/linhes-system/remove_storage.py @@ -0,0 +1,184 @@ +#!/usr/bin/python2 +#remove_storage.py removes disks that were added using add_storage.py +# +#Only disks that have a conf file in /etc/storage.d/ are presented +#as choices to remove. + + +import os,sys,subprocess +import configparser +from configparser import SafeConfigParser +import glob +from socket import gethostname +from MythTV import MythDB + +storage_dir = "/etc/storage.d" + +def runcmd(cmd): + if True : + pass + else: + cmd = "echo "+cmd + #print cmd + cmdout = subprocess.getstatusoutput(cmd) + #logging.debug(" %s", cmdout) + return cmdout + +def read_fstab(): + f = open('/etc/fstab', 'r') + fstab=f.readlines() + f.close() + return fstab + +def unmount_disk(conf): + if os.path.ismount(conf[1]): + print("Unmounting " + conf[1] + ".") + cmd = "umount %s" %conf[1] + runcmd(cmd) + +def remove_fstab_entry(conf): + fstab=read_fstab() + f = open('/etc/fstab', 'w') + for line in fstab: + if not line.startswith("#"): + if line.find(conf[1]) > -1 and line.find(conf[2]) > -1 : + print("Removing " + conf[1] + " from fstab.") + else: + f.write(line) + else: + f.write(line) + f.close() + +def remove_SG_entries(conf): + DB = MythDB() + host=gethostname() + with DB as c: + print("Removing " + conf[1] + " storage group\n paths from MythTV database.") + c.execute("""delete from storagegroup where hostname = %s and dirname like %s""", (host,conf[1] + '%')) + c.execute("""delete from settings where hostname = %s and value like %s""", (host,'SGweightPerDir:' + host + ':' + conf[1] + '%')) + +def remove_disk_link(conf): + if os.path.islink("/data/storage/disk" + str(conf[0])): + print("Removing link /data/storage/disk%s." %(conf[0])) + cmd = "rm -f /data/storage/disk%s" %(conf[0]) + runcmd(cmd) + +def remove_disk_mount(conf): + print("Removing mountpoint %s." %(conf[1])) + cmd = "rm -rf %s" %(conf[1]) + runcmd(cmd) + +def update_conf_file(conf): + print("Updating %s file\n and running systemconfig." %(conf[4])) + parser = SafeConfigParser() + parser.read(conf[4]) + parser.set('storage','shareable',"False") + parser.set('storage','removed',"True") + with open(conf[4], 'wb') as conf[4]: + parser.write(conf[4]) + cmd = "systemconfig.py -m fileshare" + runcmd(cmd) + +def usage(): + help=''' + remove_storage.py removes disks that were added using add_storage.py. + Only disks that have a conf file in /etc/storage.d/ are presented + as choices to remove. + + Disks removed from the system are not erased or formatted. + + Normal operations include (in this order): + Ask which disk to remove + Unmount the disk if mounted + Remove disk from /etc/fstab + Remove MythTV storage group paths in MythTV database + Remove disk# symlink at /data/storage/ + Remove disk name mountpoint at /data/storage/ + Make shareable = False in /etc/storage.d/DISKNAME.conf + Add removed = True in /etc/stoarge.d/DISKNAME.conf + Run systemconfig.py -m fileshare to update File Shares + + Options: + -h, --help: Show this help message. + ''' + print(help) + sys.exit(0) + +#-------------------------------------------- + +def main(): + all_confs = [] + + # get conf files that are not disk0 or not removed + for conf_file in glob.glob('%s/*.conf' %storage_dir): + this_conf = [] + parser = SafeConfigParser() + parser.read(conf_file) + mounted = "" + disk_num = parser.get('storage', 'disk_num') + try: + removed = parser.get('storage', 'removed') + except: + removed = False + if disk_num == "0" or removed: + continue + this_conf.append(int(disk_num)) #0 + this_conf.append(parser.get('storage', 'mountpoint')) #1 + this_conf.append(parser.get('storage', 'uuid')) #2 + if os.path.ismount(this_conf[1]): + this_conf.append("(mounted)") #3 + else: + this_conf.append("") #3 + this_conf.append(conf_file) #4 + all_confs.append(this_conf) + + # sort confs on disk num + all_confs.sort(key=lambda x: x[0]) + + # exit if no disks found + if len(all_confs) == 0: + print("\nThere are no disks to remove. Exiting.") + sys.exit(0) + + print("\nDisks found in /etc/storage.d/:\n") + + # print list of disks to remove + for i,conf in enumerate(all_confs): + print(str(i+1) + ": " + conf[1] + " (disk" + str(conf[0]) + ") " + conf[3]) + + # get user input of disk to remove + try: + conf_select=input("\nEnter the number of the disk to remove: ") + conf_select=int(conf_select) + if conf_select > len(all_confs) or conf_select < 1: + conf_select=int("e") + except ValueError: + print("You must enter a number between 1 and " + str(len(all_confs)) + ". Exiting.") + sys.exit(0) + + selected_conf = all_confs[conf_select-1] + + # confirm selection + confirm_select=input("\nDisk " + selected_conf[1] + " (disk" + str(selected_conf[0]) + ") will be removed.\nAre you sure you want to remove it (yes/no)? ") + if confirm_select != "yes": + print("Exiting.") + sys.exit(0) + + print("") + unmount_disk(selected_conf) + remove_fstab_entry(selected_conf) + remove_SG_entries(selected_conf) + remove_disk_link(selected_conf) + remove_disk_mount(selected_conf) + update_conf_file(selected_conf) + + print("\nDisk " + selected_conf[1] + " has been removed.") + +if __name__ == "__main__": + if not os.geteuid()==0: + sys.exit("\nRoot access is required to run this program.\n") + + if "--help" in sys.argv or "-h" in sys.argv: + usage() + else: + main() diff --git a/linhes/linhes-system/system-sudo.rules b/linhes/linhes-system/system-sudo.rules new file mode 100644 index 0000000..86e1b72 --- /dev/null +++ b/linhes/linhes-system/system-sudo.rules @@ -0,0 +1 @@ +mythtv ALL=(ALL) NOPASSWD: ALL diff --git a/linhes/linhes-system/xfs_defrag.cron b/linhes/linhes-system/xfs_defrag.cron new file mode 100644 index 0000000..792a4b4 --- /dev/null +++ b/linhes/linhes-system/xfs_defrag.cron @@ -0,0 +1,3 @@ +#!/bin/bash +#. /etc/profile +/usr/LH/bin/checkXFSfrag.sh --idle | /usr/bin/logger -t xfs_defrag -- cgit v0.12