├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── nightlybuild.yml ├── .gitignore ├── LICENSE ├── README.md ├── media └── screenshot_main.png ├── package └── veeamhubrepo │ ├── DEBIAN │ └── control │ └── usr │ ├── bin │ ├── .gitkeep │ └── veeamhubrepo │ └── share │ └── veeamhubrepo │ └── .gitkeep └── src ├── dialogwrappers.py ├── requirements.txt ├── veeamhubrepo.py └── veeamhubutil.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Package 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Get version 14 | run: "echo \"VEEAMHUBREPOVERSION=$(cat src/veeamhubrepo.py | grep -e '# VEEAMHUBREPOVERSION:' | sed 's/# VEEAMHUBREPOVERSION: //;s/\\s*//g')\" >> $GITHUB_ENV" 15 | 16 | - name: Mark version 17 | run: 'echo "${{env.VEEAMHUBREPOVERSION}} GA" > package/veeamhubrepo/usr/share/veeamhubrepo/release.txt' 18 | 19 | - name: Copy file 20 | run: "cp src/* package/veeamhubrepo/usr/share/veeamhubrepo" 21 | 22 | - name: Set version in package 23 | run: 'sed -i "s/Version: .*/Version: ${{env.VEEAMHUBREPOVERSION}}/" package/veeamhubrepo/DEBIAN/control' 24 | - name: Cleanup gitkeep 25 | run: 'find . -iname ".gitkeep" -exec rm {} \;' 26 | 27 | - name: Build Packages 28 | run: "dpkg-deb --build package/veeamhubrepo" 29 | 30 | - name: Rename 31 | run: "mkdir dist && mv package/veeamhubrepo.deb dist/veeamhubrepo_noarch_${{env.VEEAMHUBREPOVERSION}}.deb" 32 | - name: Show Output 33 | run: find dist/ && find package/ 34 | 35 | - name: Upload a deb 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: "veeamhubrepo_noarch_${{env.VEEAMHUBREPOVERSION}}.deb" 39 | path: "dist/veeamhubrepo_noarch_${{env.VEEAMHUBREPOVERSION}}.deb" 40 | retention-days: 7 41 | -------------------------------------------------------------------------------- /.github/workflows/nightlybuild.yml: -------------------------------------------------------------------------------- 1 | name: "Nightly Build" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 02 * * *" 6 | workflow_dispatch: 7 | 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Get version 16 | run: "echo \"VEEAMHUBREPOVERSION=$(cat src/veeamhubrepo.py | grep -e '# VEEAMHUBREPOVERSION:' | sed 's/# VEEAMHUBREPOVERSION: //;s/\\s*//g')\" >> $GITHUB_ENV" 17 | 18 | - name: Copy file 19 | run: "cp src/* package/veeamhubrepo/usr/share/veeamhubrepo" 20 | 21 | - name: Mark version 22 | run: 'echo "${{env.VEEAMHUBREPOVERSION}} nightly $(date +%Y-%d-%m)" > package/veeamhubrepo/usr/share/veeamhubrepo/release.txt' 23 | 24 | - name: Set version in package 25 | run: 'sed -i "s/Version: .*/Version: ${{env.VEEAMHUBREPOVERSION}}/" package/veeamhubrepo/DEBIAN/control' 26 | - name: Cleanup gitkeep 27 | run: 'find . -iname ".gitkeep" -exec rm {} \;' 28 | 29 | - name: Build Packages 30 | run: "dpkg-deb --build package/veeamhubrepo" 31 | 32 | - name: Rename 33 | run: "mkdir dist && mv package/veeamhubrepo.deb dist/veeamhubrepo_noarch_${{env.VEEAMHUBREPOVERSION}}.deb" 34 | - name: Show Output 35 | run: find dist/ && find package/ 36 | 37 | - name: Upload a deb 38 | uses: actions/upload-artifact@v2.2.3 39 | with: 40 | name: "veeamhubrepo_noarch_${{env.VEEAMHUBREPOVERSION}}_nightly" 41 | path: "dist/*.deb" 42 | retention-days: 7 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | .vscode/ 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 tdewin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VeeamHub Repo 2 | 3 | Experimental Python script to quickly setup an immutable repository. Initially made to quickly setup demo labs but feedback is appreciated. Tested only on Ubuntu 20.04 LTS (and this is the only target for this project until the next LTS). 4 | 5 | Check the release page to get debian package 6 | 7 | Easy install by using wget to download the package and sudo apt-get install ./veeamhubrepo.deb to install it e.g 8 | ``` 9 | sudo wget -O ./veeamhubrepo.deb https://github.com/tdewin/veeamhubrepo/releases/download/v0.3.2/veeamhubrepo_noarch.deb 10 | sudo apt-get install ./veeamhubrepo.deb 11 | sudo veeamhubrepo 12 | ``` 13 | 14 | How it works: 15 | - Deploy Ubuntu 20.04 LTS on disk 1 (16GB is more then enough, you only need the base install) 16 | - Add a secondary repository disk/block device 17 | - Deploy Veeamhub Repo 18 | - Launch Veeamhub Repo 19 | - A wizard will start that should detect the second disk/block device via lsblk and ask you to format it (experimental project be careful) 20 | - Additionally it will create a new unpriveleged user, disable SSH, configure the firewall, .. 21 | - Add the end of the wizard it will ask you to enable SSH. Before you start this process, make sure that you are ready to add the repository to a Veeam V11 installation 22 | - Enable SSH, the wizard will show you the configured IP, user, etc. as a reminder. Go to V11 and register the repo with "Single use credentials" and "elevate automatically" checked 23 | - Once you click through, the GUI should detect that the repository is added, auto close SSH and remove sudo power to the veeamrepo user. 24 | 25 | Next time you open the VeeamHub repo manager, it will allow you to modify settings, read logs or monitor space usage 26 | 27 | 28 | If you did a clean install, you install the packages and it complains about depencies, please run an apt-get update 29 | ``` 30 | sudo apt-get update 31 | ``` 32 | 33 | Alternatively you can just download the python script but the advantage with the package is that it will put everything in the correct location and it will download all dependecies. 34 | 35 | 36 | # FAQ 37 | 38 | Q: Why is the initial release 0.3.1 39 | A: This was an internal project so that other engineers quickly could get started with Linux immutable repositories 40 | 41 | 42 | # Screenshot 43 | 44 | ![Main Menu](https://raw.githubusercontent.com/tdewin/veeamhubrepo/main/media/screenshot_main.png) 45 | 46 | MIT License 47 | 48 | -------------------------------------------------------------------------------- /media/screenshot_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdewin/veeamhubrepo/0a7538ff6d9abbf30a7c5956b63d36bd3d4668e8/media/screenshot_main.png -------------------------------------------------------------------------------- /package/veeamhubrepo/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: veeamhubrepo 2 | Version: 0.3.1 3 | Section: custom 4 | Priority: optional 5 | Architecture: all 6 | Essential: no 7 | Installed-Size: 1024 8 | Maintainer: github.com/tdewin 9 | Depends: python3-dialog, python3-pystemd, openssh-server, python3-psutil, nano, python3-yaml, python3-humanize 10 | Description: simple managers for immutable veeam repositories 11 | -------------------------------------------------------------------------------- /package/veeamhubrepo/usr/bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdewin/veeamhubrepo/0a7538ff6d9abbf30a7c5956b63d36bd3d4668e8/package/veeamhubrepo/usr/bin/.gitkeep -------------------------------------------------------------------------------- /package/veeamhubrepo/usr/bin/veeamhubrepo: -------------------------------------------------------------------------------- 1 | /usr/share/veeamhubrepo/veeamhubrepo.py -------------------------------------------------------------------------------- /package/veeamhubrepo/usr/share/veeamhubrepo/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdewin/veeamhubrepo/0a7538ff6d9abbf30a7c5956b63d36bd3d4668e8/package/veeamhubrepo/usr/share/veeamhubrepo/.gitkeep -------------------------------------------------------------------------------- /src/dialogwrappers.py: -------------------------------------------------------------------------------- 1 | from dialog import Dialog 2 | import os 3 | import getpass 4 | 5 | 6 | def screensize(): 7 | srows,scolumns = os.popen('stty size','r').read().split() 8 | return int(srows),int(scolumns) 9 | 10 | class DialogWrapper: 11 | OK=1 12 | title="" 13 | Cancel=-1 14 | style="dialog" 15 | 16 | def __init__(self,title): 17 | self.title = "VeeamHub Tiny Repo Manager" 18 | self.dialog = Dialog(dialog="dialog") 19 | self.dialog.set_background_title(self.title) 20 | self.OK = self.dialog.OK 21 | 22 | def infobox(self,infotext,width=80,height=10): 23 | return self.dialog.infobox(infotext,width=width,height=height) 24 | 25 | def msgbox(self,infotext,width=80,height=10): 26 | return self.dialog.msgbox(infotext,width=width,height=height) 27 | 28 | 29 | def passwordbox(self,infotext,insecure=True): 30 | return self.dialog.passwordbox(infotext,insecure=insecure) 31 | 32 | def inputbox(self,infotext,init=""): 33 | return self.dialog.inputbox(infotext,init=init,width=80) 34 | 35 | def yesno(self,question,width=80,height=10,yes_label="yes",no_label="no"): 36 | return self.dialog.yesno(question,width=80,height=10,yes_label=yes_label,no_label=no_label) 37 | 38 | def menu(self,text,choices,height=15,cancel="Cancel"): 39 | return self.dialog.menu(text,choices=choices,height=height,cancel=cancel) 40 | 41 | def checklist(self,text,choices,height=15,cancel="Cancel"): 42 | return self.dialog.checklist(text,choices=choices,height=height,cancel=cancel) 43 | 44 | def fselect(self,path,width=80,height=20): 45 | self.msgbox("Easiest way is to type the path while browsing\nUse / as dir seperator\n\nAlternatively try tab+arrow keys to navigate\nand space to copy",width=60) 46 | return self.dialog.fselect(path,width=width,height=height) 47 | 48 | 49 | class AlternateDialog(DialogWrapper): 50 | style="alternate" 51 | rows=0 52 | columns=0 53 | 54 | def __init__(self,title,rows,columns): 55 | self.rows = rows 56 | self.columns = columns 57 | DialogWrapper.__init__(self,title) 58 | 59 | def header(self): 60 | print("{}".format(self.title)) 61 | 62 | def lnspacer(self): 63 | dasher = [] 64 | dash = "-" 65 | for i in range(int(self.columns/4*3)): 66 | dasher.append(dash) 67 | print("".join(dasher)) 68 | 69 | def cls(self): 70 | os.system('clear') 71 | self.rows,self.columns = screensize() 72 | self.header() 73 | self.lnspacer() 74 | 75 | 76 | def passwordbox(self,infotext,insecure=True): 77 | self.cls() 78 | print(infotext) 79 | self.lnspacer() 80 | passw = getpass.getpass(prompt="password : ") 81 | return self.OK,passw 82 | 83 | 84 | def infobox(self,infotext,width=80,height=10): 85 | self.cls() 86 | print(infotext) 87 | self.lnspacer() 88 | 89 | def yesno(self,question,width=80,height=10,yes_label="yes",no_label="no"): 90 | code = self.OK 91 | test = "o" 92 | while not (test == "" or test == "e"): 93 | self.cls() 94 | print(question) 95 | self.lnspacer() 96 | test = input("Press <> for {}, enter <> for {} : ".format(yes_label,no_label)) 97 | if test == "e": 98 | code = self.Cancel 99 | return code 100 | 101 | def msgbox(self,infotext,width=80,height=10): 102 | self.cls() 103 | print(infotext) 104 | self.lnspacer() 105 | input("Press enter to continue..") 106 | 107 | def inputbox(self,infotext,init=""): 108 | self.cls() 109 | print(infotext) 110 | self.lnspacer() 111 | code = self.OK 112 | answer = "" 113 | try: 114 | answer = input("(default <<{}>>, ctrl+c to exit): ".format(init)) 115 | if answer == "": 116 | answer = init 117 | except KeyboardInterrupt: 118 | code = self.Cancel 119 | 120 | return code,answer 121 | 122 | def menu(self,text,choices,height=15,cancel="Cancel"): 123 | valid = ["e"] 124 | for c in choices: 125 | valid.append(c[0]) 126 | 127 | userinput = "" 128 | while not userinput in valid: 129 | self.cls() 130 | print(text) 131 | self.lnspacer() 132 | for c in choices: 133 | print("{}) {}".format(c[0],c[1])) 134 | self.lnspacer() 135 | userinput = input("Please specify choice or <> to exit:") 136 | 137 | code = self.OK 138 | if userinput == "e": 139 | code = self.Cancel 140 | 141 | return code,userinput 142 | 143 | def checklist(self,text,choices,height=15,cancel="Cancel"): 144 | valid = ["e"] 145 | for c in choices: 146 | valid.append(c[0]) 147 | 148 | userinput = "" 149 | expl = [] 150 | allvalid = False 151 | while not allvalid: 152 | self.cls() 153 | print(text) 154 | self.lnspacer() 155 | for c in choices: 156 | print("{}) {}".format(c[0],c[1])) 157 | self.lnspacer() 158 | print("Multi select by using ',' as a seperator e.g 1,2") 159 | userinput = input("Please specify choice or <> to exit:") 160 | expl = userinput.split(",") 161 | testvalid = True 162 | for t in expl: 163 | if not t in valid: 164 | testvalid = False 165 | if testvalid and len(expl) > 0: 166 | allvalid = True 167 | 168 | code = self.OK 169 | if userinput == "e": 170 | code = self.Cancel 171 | 172 | return code,expl 173 | 174 | 175 | 176 | def fselect(self,path,width=80,height=20): 177 | self.cls() 178 | path = path.rstrip("/") 179 | print("Current path is {}".format(path)) 180 | filesall = [] 181 | fileindex = {} 182 | selecti = 1 183 | for dirwalk in os.walk(path): 184 | for fwalk in dirwalk[2]: 185 | localpath = dirwalk[0].replace(path,"") 186 | fname = fwalk 187 | if localpath != "": 188 | fname = "{}/{}".format(dirwalk[0].replace(path,""),fwalk) 189 | 190 | fobject = [fname,"{}/{}".format(dirwalk[0],fwalk),selecti] 191 | filesall.append(fobject) 192 | fileindex[selecti]=fobject 193 | selecti = selecti+1 194 | 195 | selected = -1 196 | showing = 0 197 | maxln = self.rows-5 198 | if maxln < 15: 199 | maxln = 20 200 | 201 | search = "" 202 | 203 | filteredfiles = filesall 204 | 205 | while selected == -1: 206 | showingend = showing + maxln 207 | nextround = showingend 208 | #if we are the end of the file list 209 | if showingend >= len(filteredfiles): 210 | showingend = len(filteredfiles) 211 | nextround = 0 212 | 213 | for i in range(showing,showingend): 214 | print("{}) {}".format(filteredfiles[i][2],filteredfiles[i][0])) 215 | 216 | 217 | if search == "": 218 | print("Use / e.g /myfile to filter for a specific file") 219 | else: 220 | print("Current search is {}, use / to reset".format(search)) 221 | 222 | selecttest = input("Please number to select or enter for more ({}) : ".format(len(filteredfiles))) 223 | if selecttest != "": 224 | if selecttest == "e": 225 | return self.Cancel,"" 226 | elif selecttest[0] == "/": 227 | search = selecttest[1:].strip() 228 | if search == "": 229 | filteredfiles = filesall 230 | else: 231 | filteredfiles = [] 232 | for f in filesall: 233 | if search in f[1]: 234 | filteredfiles.append(f) 235 | nextround = 0 236 | else: 237 | try: 238 | intval = int(selecttest) 239 | if intval in fileindex: 240 | selected = fileindex[intval] 241 | except ValueError: 242 | print("Please enter an integer or hit enter to see more") 243 | 244 | showing = nextround 245 | 246 | fileselected = selected[1] 247 | code = self.yesno("Selecting {}".format(fileselected)) 248 | 249 | return code,fileselected 250 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | netifaces==0.10.9 2 | psutil==5.8.0 3 | pythondialog==3.5.1 4 | PyYAML==5.3.1 5 | humanize==1.0.0 6 | -------------------------------------------------------------------------------- /src/veeamhubrepo.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Veeamhub Repo 4 | # VEEAMHUBREPOVERSION: 0.3.4 5 | 6 | import locale 7 | from pathlib import Path 8 | import json 9 | import re 10 | import subprocess 11 | import shutil 12 | import netifaces 13 | import pystemd 14 | import psutil 15 | import os 16 | import glob 17 | import sys 18 | import time 19 | import configparser 20 | import datetime 21 | import getpass 22 | import yaml 23 | import ipaddress 24 | 25 | #local imports 26 | # 27 | import dialogwrappers 28 | import veeamhubutil 29 | 30 | # Start menu in home() function at the bottom 31 | 32 | # Open a file for reading, by default trying to use config reader 33 | # By default nano is set in the config file 34 | # Nano is a bit nicer then the textbox functionality 35 | def readfile(config,d,path): 36 | if "reader" in config and shutil.which(config["reader"][0]) is not None: 37 | procrun = config["reader"] + [path] 38 | subprocess.call(procrun) 39 | else: 40 | d.textbox(path,width=80,height=30) 41 | # Open a file for wrting 42 | def openfile(config,d,path): 43 | if "writer" in config and shutil.which(config["writer"][0]) is not None: 44 | procrun = config["writer"] + [path] 45 | subprocess.call(procrun) 46 | else: 47 | d.editbox(path,width=80,height=30) 48 | 49 | 50 | def removepackage(d,packagename): 51 | d.infobox("Removing package {}".format(packagename)) 52 | try: 53 | veeamhubutil.removepackage(packagename) 54 | except Exception as e: 55 | d.msgbox(e.args[0]) 56 | 57 | 58 | def installpackage(d,packagename): 59 | error = False 60 | 61 | d.infobox("Installing package {}".format(packagename)) 62 | try: 63 | veeamhubutil.installpackage(packagename) 64 | except Exception as e: 65 | d.msgbox(e.args[0]) 66 | error = True 67 | 68 | return error 69 | 70 | 71 | 72 | # Menu 1 Create User 73 | # the repouser function itself 74 | def setrepouser(config,d): 75 | code,user = d.inputbox("Confirm your user",init=config["repouser"]) 76 | if code == d.OK: 77 | if not veeamhubutil.usersexists(user): 78 | code = d.yesno("User {0} does not exists, do you want to create it?".format(user)) 79 | if code == d.OK: 80 | pout = subprocess.run(["useradd","-m",user], capture_output=True) 81 | if pout.returncode != 0: 82 | d.msgbox("Error creating user {0}".format(str(pout.stderr,'utf-8'))) 83 | return 84 | else: 85 | code,pwd = d.passwordbox("Enter password for user",insecure=True) 86 | pout = subprocess.run(["chpasswd"],input="{}:{}".format(user,pwd).encode('utf-8'),capture_output=True) 87 | if pout.returncode != 0: 88 | d.msgbox("Error setting passwd {0}".format(str(pout.stderr,'utf-8'))) 89 | return 90 | else: 91 | d.msgbox("User created") 92 | config["repouser"] = user 93 | 94 | # Menu 2 Format drive 95 | # the format function itself 96 | def formatdrive(config,d): 97 | choices = [("1","Manually define disk")] 98 | shadow = {1:"/dev/"} 99 | i = 2 100 | 101 | repoadded = "" 102 | 103 | for device in veeamhubutil.lsblk(): 104 | choices.append((str(i),device.MenuEntry())) 105 | shadow[i] = device.Path 106 | i=i+1 107 | 108 | code, tag = d.menu("Select partition to format", choices=choices) 109 | if code == d.OK: 110 | code,part = d.inputbox("Confirm selected partition", init=shadow[int(tag)]) 111 | if code == d.OK: 112 | code = d.yesno(f"Are you sure you want to format {part}?") 113 | if code == d.OK: 114 | #trying to suggest to make a primary partition on a complete disk 115 | if re.match("/dev/.d[a-z]{1,2}$", part): 116 | code = d.yesno(f"{part} contains no partitions. Do you want to partition it?") 117 | if code == d.OK: 118 | if shutil.which("parted") is not None: 119 | try: 120 | pout = subprocess.run([ 121 | "parted","-s",part, 122 | "mklabel","gpt", 123 | "mkpart", "primary", "0%", "100%" 124 | ], capture_output=True) 125 | part = f"{part}1" 126 | except subprocess.CalledProcessError as e: 127 | d.msgbox( 128 | f"Error partitioning {part}, code {e.returncode}:" 129 | f"{e.stderr}".encode("utf-8") 130 | ) 131 | return 1, repoadded 132 | else: 133 | d.msgbox("Parted not found, stopping") 134 | return 1,repoadded 135 | 136 | d.infobox(f"Now formating {part} with XFS") 137 | #sometimes disk is not yet synced if too fast 138 | time.sleep(2) 139 | pout = subprocess.run(["mkfs.xfs","-b","size=4096","-m","reflink=1,crc=1",part], capture_output=True) 140 | if pout.returncode != 0: 141 | force = d.yesno("mkfs.xfs failed, do you want me to try to force it?\n{0}".format(str(pout.stderr,'utf-8')),width=80) 142 | if force == d.OK: 143 | pout = subprocess.run(["mkfs.xfs","-f","-b","size=4096","-m","reflink=1,crc=1",part], capture_output=True) 144 | 145 | 146 | if pout.returncode != 0: 147 | d.msgbox("Error formatting {0}".format(str(pout.stderr,'utf-8'))) 148 | else: 149 | choices = [ 150 | ("1", "One digit (repo1)"), 151 | ("2", "Two digits (repo01)"), 152 | ("3", "Three digits (repo001)"), 153 | ] 154 | code, tag = d.menu("Select leading zeros in disk name", choices=choices) 155 | 156 | mountpoint = "" 157 | 158 | found = False 159 | i = 1 160 | high = (10**int(tag))-1 161 | zeros = len(str(high)) 162 | 163 | while not found and i < high: 164 | test_path = Path("/backups/disk-{}".format(str(i).zfill(zeros))) 165 | if test_path.exists(): 166 | i = i+1 167 | else: 168 | mountpoint = test_path 169 | found = True 170 | 171 | code, mountpoint = d.inputbox("Where do you want to mount the disk?",init=str(mountpoint)) 172 | if code == d.OK: 173 | p = Path(mountpoint) 174 | if p.exists(): 175 | d.msgbox("Path exists, stopping") 176 | return 1, repoadded 177 | else: 178 | p.mkdir(mode=0o770, parents=True, exist_ok=False) 179 | 180 | uuid = "" 181 | for dbid in Path("/dev/disk/by-uuid/").iterdir(): 182 | if str(dbid.resolve()) == part: 183 | uuid = dbid 184 | 185 | if uuid == "": 186 | d.msgbox("Can not resolve uuid") 187 | return 1,repoadded 188 | else: 189 | mountline = "\n{} {} xfs defaults 0 0\n".format(str(uuid),mountpoint) 190 | with open("/etc/fstab", "a") as fstab: 191 | fstab.write(mountline) 192 | 193 | pout = subprocess.run(["mount",mountpoint], capture_output=True) 194 | if pout.returncode != 0: 195 | d.msgbox("Error mounting {0}".format(str(pout.stderr,'utf-8'))) 196 | return 1,repoadded 197 | else: 198 | shutil.chown(str(p),config["repouser"],config["repouser"]) 199 | d.msgbox("{} mounted and added!".format(mountpoint)) 200 | repoadded = mountpoint 201 | return 0,repoadded 202 | 203 | return 0,repoadded 204 | 205 | 206 | # Menu 3 register the server 207 | def registerserver(config,d,wizardstart=False): 208 | forceregister = False 209 | if veeamhubutil.veeamrunning(): 210 | c = d.yesno("Veeam Transport Service is already running, do you want to continue") 211 | if c != d.OK: 212 | return 213 | else: 214 | forceregister = True 215 | 216 | # uses dbus to talk to the service, should be more stable then parsing systemctl output 217 | # enable ssh when registration starts 218 | ssh = veeamhubutil.getsshservice() 219 | sshstarted = False 220 | sshstop = False 221 | if ssh.Unit.ActiveState != b'active': 222 | code = d.OK 223 | if not wizardstart: 224 | code = d.yesno("SSH is not started, shall I temporarily start it?") 225 | 226 | if code == d.OK: 227 | ssh.Unit.Start(b'replace') 228 | sshstarted = True 229 | sshstop = True 230 | else: 231 | sshstarted = True 232 | 233 | # if ufw is on (firewall), enable ssh 234 | if not veeamhubutil.ufw_is_inactive(): 235 | veeamhubutil.ufw_ssh(setstatus="allow") 236 | 237 | #if ssh started, then add a temp sudo file giving the repouser sudo rights 238 | if sshstarted: 239 | veeamrepouser = config["repouser"] 240 | sudofp = "/etc/sudoers.d/90-{}".format(veeamrepouser) 241 | if not Path(sudofp).exists(): 242 | with open(sudofp, 'w') as outfile: 243 | outfile.write("{} ALL=(ALL:ALL) ALL".format(veeamrepouser)) 244 | os.chmod(sudofp,0o440) 245 | timeout = 500 246 | if 'registertimeout' in config: 247 | timeout = config['registertimeout'] 248 | sleeper = 5 249 | try : 250 | while (not veeamhubutil.veeamrunning() or forceregister) and timeout > 0: 251 | lines = ["Go to the backup server and connect with single use cred", 252 | "User :",config["repouser"],"" 253 | "IPs :"] 254 | lines = lines + veeamhubutil.myips() + ["","Repos:"] 255 | for repo in config['repositories']: 256 | lines.append(repo) 257 | if forceregister: 258 | lines = lines + ["","Running in forced mode"] 259 | lines = lines + ["","CTRL+C to force exit","Auto locking in {} seconds".format(timeout)] 260 | d.infobox("\n".join(lines),width=60,height=(len(lines)+10)) 261 | timeout = timeout-sleeper 262 | time.sleep(sleeper) 263 | if veeamhubutil.veeamrunning(): 264 | while veeamhubutil.veeamreposshcheck(config["repouser"]) and timeout > 0: 265 | d.infobox("Veeam Process detected\nWaiting for SSH to stop or for timeout ({})".format(timeout)) 266 | timeout = timeout-sleeper 267 | time.sleep(sleeper) 268 | except KeyboardInterrupt: 269 | d.infobox("Cleaning up") 270 | 271 | #delete sudo file and close ssh after the user 272 | Path(sudofp).unlink() 273 | if sshstop: 274 | ssh.Unit.Stop(b'replace') 275 | 276 | if not veeamhubutil.ufw_is_inactive(): 277 | veeamhubutil.ufw_ssh(setstatus="deny") 278 | else: 279 | d.msgbox("Sudo file already exists, possible breach, please clean up by using sudo rm {0}".format(sudofp)) 280 | else: 281 | d.msgbox("SSH not started, can not continue") 282 | 283 | 284 | # Menu 4, monitor box 285 | # 4.1 286 | def checkspace(config,d): 287 | cont = d.OK 288 | while cont == d.OK: 289 | ln = ["Repositories:"] 290 | for repo in config['repositories']: 291 | stat = shutil.disk_usage(repo) 292 | #total=53658783744, used=407994368, free=53250789376 293 | ln.append("{:40} {}% {}GB".format(repo,int(stat.used*10000/stat.total)/100,int(stat.total/1024/1024/1024))) 294 | 295 | cont = d.yesno("\n".join(ln),width=60,height=len(ln)+4,yes_label="Refresh",no_label="Cancel") 296 | print(cont) 297 | # 4.2 298 | def checklogs(config,d): 299 | code = d.OK 300 | path = "/var/log/" 301 | 302 | if Path("/var/log/VeeamBackup/").is_dir(): 303 | path = "/var/log/VeeamBackup/" 304 | 305 | while code == d.OK and not Path(path).is_file(): 306 | code, path = d.fselect(path,width=80,height=30) 307 | 308 | if code == d.OK: 309 | readfile(config,d,path) 310 | 311 | # 4.3 312 | def checkproc(config,d): 313 | cont = d.OK 314 | 315 | procfilter = "veeam" 316 | while cont == d.OK: 317 | ln = [] 318 | procs = [p for p in psutil.process_iter(['pid', 'name', 'username']) if procfilter in p.name()] 319 | if len(procs) > 0: 320 | for proc in procs: 321 | ln.append("{} {} {}".format(proc.pid,proc.username(),proc.name())) 322 | else: 323 | ln = ["No processes found matching 'veeam'","Did you add this repo?"] 324 | cont = d.yesno("\n".join(ln),width=60,height=len(ln)+4,yes_label="Refresh",no_label="Cancel") 325 | 326 | # 4 main 327 | def monitorrepos(config,d): 328 | code = d.OK 329 | while code == d.OK: 330 | code, tag = d.menu("What do you want to do:", 331 | choices=[("1", "Check Disk Space"), 332 | ("2", "Check Logs"), 333 | ("3", "Show Veeam Processes"), 334 | ]) 335 | if code == d.OK: 336 | if tag == "1": 337 | checkspace(config,d) 338 | elif tag == "2": 339 | checklogs(config,d) 340 | elif tag == "3": 341 | checkproc(config,d) 342 | 343 | # Menu 5, add repo path 344 | def managerepo(config,d): 345 | updated = False 346 | code = d.OK 347 | while code == d.OK: 348 | code, tag = d.menu("What do you want to do:", 349 | choices=[("1", "Add A Repo"), 350 | ("2", "Delete A repo"), 351 | ]) 352 | if code == d.OK: 353 | if tag == "1": 354 | acode,mountpoint = d.inputbox("Which path do you want to add?",init="/backups/repo") 355 | if acode == d.OK: 356 | if not Path(mountpoint).is_dir(): 357 | d.msgbox("{} is not a path or dir".format(mountpoint)) 358 | else: 359 | d.msgbox("{} added".format(mountpoint)) 360 | config['repositories'].append(mountpoint) 361 | updated = True 362 | elif tag == "2": 363 | if len(config['repositories']) == 0: 364 | d.msgbox("No repo's added yet") 365 | else: 366 | listrepo = [] 367 | shadow = {} 368 | i = 1 369 | for repo in config['repositories']: 370 | listrepo.append((str(i),repo)) 371 | shadow[str(i)] = repo 372 | i = i+1 373 | dcode, dtag = d.menu("What repository do you want to remove from config:",choices=listrepo) 374 | if dcode == d.OK: 375 | config['repositories'].remove(shadow[dtag]) 376 | updated = True 377 | 378 | return updated 379 | 380 | # Menu 6 manage ubuntu 381 | # 6.1 update 382 | def update(config,d): 383 | if shutil.which("apt-get") is not None: 384 | d.infobox("Checking updates...") 385 | pout = subprocess.run(["apt-get","update","-y"], capture_output=True) 386 | if pout.returncode != 0: 387 | d.msgbox("Error updating {0}".format(str(pout.stderr,'utf-8'))) 388 | return 389 | d.infobox("Running Update...") 390 | pout = subprocess.run(["apt-get","upgrade","-y"], capture_output=True) 391 | if pout.returncode != 0: 392 | d.msgbox("Error updating {0}".format(str(pout.stderr,'utf-8'))) 393 | return 394 | d.infobox("Update Completed") 395 | time.sleep(1) 396 | else: 397 | d.infobox("Apt-get not found, not executing update") 398 | time.sleep(5) 399 | 400 | # 6.2 harden 401 | def disablessh(): 402 | ssh = veeamhubutil.getsshservice() 403 | if ssh.Unit.ActiveState == b'active': 404 | ssh.Unit.Stop(b'replace') 405 | 406 | mgr = pystemd.systemd1.Manager() 407 | mgr.load() 408 | if len([serv for serv in mgr.Manager.ListUnitFiles() if "sshd" in str(serv[0]) ]) > 0: 409 | mgr.Manager.DisableUnitFiles([b'sshd.service'],False) 410 | if not veeamhubutil.ufw_is_inactive(): 411 | veeamhubutil.ufw_ssh(setstatus="deny") 412 | 413 | def enablefw(): 414 | if veeamhubutil.ufw_is_inactive(): 415 | veeamhubutil.ufw_activate() 416 | 417 | def harden(config,d): 418 | code, tag = d.menu("What do you want to do:", 419 | choices=[("1", "Enable UFW"), 420 | ("2", "Stop and disable SSH"), 421 | ("3", "Temporarly enable SSH")]) 422 | if code == d.OK: 423 | if tag == "1": 424 | enablefw() 425 | elif tag == "2": 426 | disablessh() 427 | elif tag == "3": 428 | ssh = veeamhubutil.getsshservice() 429 | if ssh.Unit.ActiveState != b'active': 430 | ssh.Unit.Start(b'replace') 431 | if not veeamhubutil.ufw_is_inactive(): 432 | veeamhubutil.ufw_ssh(setstatus="allow") 433 | d.msgbox("SSH started but not on reboot\nDo not forget to Stop it again!!",width=60) 434 | 435 | # 6.3 time 436 | # 6.3.1 timezone 437 | 438 | def configtimezone(config,d): 439 | code = d.OK 440 | path = "/usr/share/zoneinfo/" 441 | while code == d.OK and not Path(path).is_file(): 442 | code, path = d.fselect(path,width=80,height=30) 443 | 444 | if code == d.OK: 445 | zi = re.match("^/usr/share/zoneinfo/(.*)",path) 446 | if zi: 447 | szi = zi.group(1) 448 | d.msgbox("Time zone set to {0}".format(szi)) 449 | 450 | pout = subprocess.run(["timedatectl","set-timezone",szi], capture_output=True) 451 | if pout.returncode != 0: 452 | d.msgbox("Error updating {0}".format(str(pout.stderr,'utf-8'))) 453 | #symlink doesnt want existing 454 | #os.symlink(path,"/etc/localtime.new") 455 | #os.rename("/etc/localtime.new","/etc/localtime") 456 | #with open("/etc/timezone", 'w') as tz: 457 | # tz.write(zi.group(1)) 458 | 459 | else: 460 | d.msgbox("Not a time zone file ({0})".format(path)) 461 | 462 | # 6.3.2 time 463 | def settime(config,d,time,date,zone,ntpactive): 464 | if ntpactive: 465 | y = d.yesno("NTP is already active\nShould I temporarily stop it?") 466 | if y != d.OK: 467 | return 468 | else: 469 | tsd = pystemd.systemd1.Unit(b'systemd-timesyncd.service') 470 | tsd.load() 471 | tsd.Unit.Stop(b'replace') 472 | 473 | y,nt = d.inputbox("Define time hh:mm:ss({0})".format(zone),init=time) 474 | if y == d.OK: 475 | if not re.match("[0-9]{2}:[0-9]{2}:[0-9]{2}",nt): 476 | d.msgbox("Error matching time, please make sure to use leading zeroes\nGiven : {0}".format(nt)) 477 | return 478 | else: 479 | y,nd = d.inputbox("Define date yyyy-mm-dd",init=date) 480 | if y == d.OK: 481 | if not re.match("[0-9]{4}-[0-9]{2}-[0-9]{2}",nd): 482 | d.msgbox("Error matching date, please make sure to use leading zeroes\nGiven : {0}".format(nd)) 483 | return 484 | else: 485 | pout = subprocess.run(["timedatectl","set-time","{} {}".format(nd,nt)], capture_output=True) 486 | if pout.returncode != 0: 487 | d.msgbox(str(pout.stderr,'utf-8')) 488 | 489 | # 6.3.3 disable ntp 490 | def disablentp(config,d): 491 | if veeamhubutil.packagetest("systemd-timesyncd") != 1: 492 | d.msgbox("NTP is not installed (timesyncd), doing nothing") 493 | else: 494 | tsd = pystemd.systemd1.Unit(b'systemd-timesyncd.service') 495 | tsd.load() 496 | if tsd.Unit.ActiveState == b'active': 497 | tsd.Unit.Stop(b'replace') 498 | 499 | mgr = pystemd.systemd1.Manager() 500 | mgr.load() 501 | if len([serv for serv in mgr.Manager.ListUnitFiles() if "timesyncd" in str(serv[0]) ]) > 0: 502 | mgr.Manager.DisableUnitFiles([b'systemd-timesyncd.service'],False) 503 | 504 | 505 | d.msgbox("Unloaded and disabled timesyncd") 506 | 507 | # 6.3.4 ntp 508 | def ntp(config,d): 509 | if veeamhubutil.packagetest("systemd-timesyncd") != 1: 510 | c = d.yesno("Systemd-timesyncd does not seem to be install it\nDo you want to install it?") 511 | if c != d.OK: 512 | return 513 | else: 514 | if installpackage(d,"systemd-timesyncd"): 515 | return 516 | 517 | ntpset = "" 518 | failoverntp = "ntp.ubuntu.com" 519 | 520 | cfile = "/etc/systemd/timesyncd.conf" 521 | # timesyncd seems to be a kind of ini file which can be easily parsed. By default it is completely commented out with only the "Time" section 522 | config = configparser.ConfigParser() 523 | config.optionxform=str 524 | config.read(cfile) 525 | if 'Time' in config.sections(): 526 | if "NTP" in config['Time']: 527 | ntpset = config['Time']['NTP'] 528 | if "FallbackNTP" in config['Time']: 529 | failoverntp = config['Time']['FallbackNTP'] 530 | 531 | y,newntpset = d.inputbox("NTP server\n(space separated)",init=ntpset) 532 | if y == d.OK: 533 | y,newfailover = d.inputbox("Failover NTP server\n(space separated)",init=failoverntp) 534 | if y == d.OK: 535 | if newntpset != ntpset or newfailover != failoverntp: 536 | shutil.copyfile(cfile, "{}.{}.backup".format(cfile,datetime.datetime.now().strftime("%Y%m%d%H%M%S"))) 537 | config['Time']['NTP'] = newntpset 538 | config['Time']['FallbackNTP'] = newfailover 539 | 540 | d.infobox("Updating..") 541 | with open(cfile, 'w') as configfile: 542 | config.write(configfile) 543 | 544 | 545 | d.infobox("Reloading timesync service") 546 | tsd = pystemd.systemd1.Unit(b'systemd-timesyncd.service') 547 | tsd.load() 548 | tsd.Unit.Stop(b'replace') 549 | tsd.Unit.Start(b'replace') 550 | 551 | mgr = pystemd.systemd1.Manager() 552 | mgr.load() 553 | mgr.Manager.EnableUnitFiles([b'systemd-timesyncd.service'],False,True) 554 | 555 | # 6.3 main menu 556 | 557 | def managetime(config,d): 558 | code = d.OK 559 | while code == d.OK: 560 | ln,time,date,zone,ntpactive = veeamhubutil.gettimeinfo() 561 | ln.append("") 562 | ln.append("What do you want to do") 563 | code, tag = d.menu("\n".join(ln), 564 | choices=[("1", "Configure Timezone"), 565 | ("2", "Manually Set Time"), 566 | ("3", "Disable NTP (timesyncd)"), 567 | ("4", "Configure NTP (timesyncd)") 568 | ],height=len(ln)+9) 569 | if code == d.OK: 570 | if tag == "1": 571 | configtimezone(config,d) 572 | elif tag == "2": 573 | settime(config,d,time,date,zone,ntpactive) 574 | elif tag == "3": 575 | disablentp(config,d) 576 | elif tag == "4": 577 | ntp(config,d) 578 | 579 | def netfileselector(config,d): 580 | basedir = "/etc/netplan" 581 | files = glob.glob(basedir+"/*.yaml") 582 | lenf = len(files) 583 | if lenf == 0: 584 | d.msgbox("Error could not located yaml file") 585 | return "" 586 | elif lenf > 1: 587 | code = d.OK 588 | path = basedir 589 | while code == d.OK and not Path(path).is_file(): 590 | code, path = d.fselect(path,width=80,height=30) 591 | if code != d.OK: 592 | return "" 593 | else: 594 | return path 595 | else: 596 | return files[0] 597 | 598 | 599 | def applynetplanyaml(config,d,yfile,newyaml): 600 | if newyaml != "": 601 | os.rename(yfile,yfile+".backup") 602 | with open(yfile,'w') as yamlfile: 603 | yamlfile.write(newyaml) 604 | 605 | pout = subprocess.run(["netplan","apply"], capture_output=True) 606 | if pout.returncode != 0: 607 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 608 | 609 | # 6.4.1 610 | def managestaticip(config,d): 611 | file = netfileselector(config,d) 612 | if file != "": 613 | newyaml = "" 614 | with open(file,'r') as yamlfile: 615 | loadedyaml = yaml.load(yamlfile,Loader=yaml.SafeLoader) 616 | choices = [] 617 | ch = 1 618 | shadow = {} 619 | 620 | for net in veeamhubutil.realnics(): 621 | choices.append((str(ch),net)) 622 | shadow[str(ch)] = net 623 | ch = ch+1 624 | 625 | 626 | code, tag = d.menu("Which network card do you want to edit",choices=choices,height=len(choices)+11) 627 | if code == d.OK: 628 | netifsh = shadow[tag] 629 | 630 | 631 | section = "ethernets" 632 | if netifsh in loadedyaml.get("network").get("bonds"): 633 | section = "bonds" 634 | 635 | if netifsh in loadedyaml.get("network").get("bridges"): 636 | section = "bridges" 637 | 638 | netifyaml = loadedyaml.get("network").get(section).get(netifsh) 639 | 640 | 641 | addrs = veeamhubutil.firstipwithnet(netifsh) 642 | if netifyaml: 643 | addrsg = netifyaml.get('addresses') 644 | if addrsg: 645 | addrs = ",".join(addrsg) 646 | 647 | code,addrs = d.inputbox("IPv4/subnet",addrs) 648 | if code != d.OK: 649 | return 650 | 651 | gw = veeamhubutil.firstgw(netifsh) 652 | if netifyaml: 653 | gwg = netifyaml.get('gateway4') 654 | if gwg: 655 | gw = gwg 656 | 657 | code,gateway = d.inputbox("Gateway",init=gw) 658 | if code != d.OK: 659 | return 660 | 661 | dns = "8.8.8.8,8.8.4.4" 662 | search = "" 663 | 664 | if netifyaml: 665 | ns = netifyaml.get('nameservers') 666 | if ns: 667 | dnsg = ns.get('addresses') 668 | if dnsg: 669 | dns = ",".join(dnsg) 670 | 671 | searchg = ns.get('search') 672 | if searchg: 673 | search = ",".join(searchg) 674 | 675 | code,dns = d.inputbox("DNS servers seperated with ,",init=dns) 676 | if code != d.OK: 677 | return 678 | 679 | code,dnssearch = d.inputbox("DNS search seperated with ,",init=search) 680 | if code != d.OK: 681 | return 682 | 683 | code = d.yesno("\n".join(["Confirm settings","","IPv4: "+addrs,"Gateway: "+gateway,"DNS: "+dns,"DNSSearch: "+dnssearch])) 684 | if code == d.OK: 685 | ifconf = {'addresses': addrs.split(","), 'gateway4': gateway, 'nameservers': {'addresses': dns.split(","), 'search': dnssearch.split(",")}} 686 | if section == "bonds" or section == "bridges": 687 | oldconfig = loadedyaml.get('network').get(section)[netifsh] 688 | if oldconfig: 689 | if oldconfig.get('parameters'): 690 | ifconf['parameters'] = oldconfig.get('parameters') 691 | if oldconfig.get('interfaces'): 692 | ifconf['interfaces'] = oldconfig.get('interfaces') 693 | 694 | loadedyaml.get('network').get(section)[netifsh] = ifconf 695 | 696 | newyaml = yaml.dump(loadedyaml) 697 | 698 | if newyaml != "": 699 | os.rename(file,file+".backup") 700 | with open(file,'w') as yamlfile: 701 | yamlfile.write(newyaml) 702 | 703 | pout = subprocess.run(["netplan","apply"], capture_output=True) 704 | if pout.returncode != 0: 705 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 706 | 707 | #6.4.2 708 | def managedhcp(config,d): 709 | file = netfileselector(config,d) 710 | if file != "": 711 | newyaml = "" 712 | with open(file,'r') as yamlfile: 713 | loadedyaml = yaml.load(yamlfile,Loader=yaml.SafeLoader) 714 | choices = [] 715 | ch = 1 716 | shadow = {} 717 | 718 | 719 | for net in veeamhubutil.realnics(): 720 | choices.append((str(ch),net)) 721 | shadow[str(ch)] = net 722 | ch = ch+1 723 | 724 | 725 | code, tag = d.menu("Which network card do you want to edit",choices=choices,height=len(choices)+11) 726 | if code == d.OK: 727 | netifsh = shadow[tag] 728 | 729 | section = "ethernets" 730 | if netifsh in loadedyaml.get("network").get("bonds"): 731 | section = "bonds" 732 | 733 | if netifsh in loadedyaml.get("network").get("bridges"): 734 | section = "bridges" 735 | 736 | 737 | code = d.yesno("\n".join(["Confirm DHCP for",netifsh])) 738 | if code == d.OK: 739 | ifconf = {'dhcp4': True} 740 | 741 | if section == "bonds" or section == "bridges": 742 | oldconfig = loadedyaml.get('network').get(section)[netifsh] 743 | if oldconfig: 744 | if oldconfig.get('parameters'): 745 | ifconf['parameters'] = oldconfig.get('parameters') 746 | if oldconfig.get('interfaces'): 747 | ifconf['interfaces'] = oldconfig.get('interfaces') 748 | 749 | 750 | loadedyaml.get('network').get(section)[netifsh] = ifconf 751 | newyaml = yaml.dump(loadedyaml) 752 | 753 | if newyaml != "": 754 | os.rename(file,file+".backup") 755 | with open(file,'w') as yamlfile: 756 | yamlfile.write(newyaml) 757 | 758 | pout = subprocess.run(["netplan","apply"], capture_output=True) 759 | if pout.returncode != 0: 760 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 761 | 762 | def managebond(config,d): 763 | yfile = netfileselector(config,d) 764 | if yfile != "": 765 | newyaml = "" 766 | with open(yfile,'r') as yamlfile: 767 | loadedyaml = yaml.load(yamlfile,Loader=yaml.SafeLoader) 768 | choices = [] 769 | shadow = {} 770 | ch = 1 771 | 772 | for net in veeamhubutil.realnics(): 773 | choices.append((str(ch),net,0)) 774 | shadow[str(ch)] = net 775 | ch = ch+1 776 | 777 | code,choices = d.checklist("Select interfaces for your bound.\nThe first bond will be your primary!!!",choices=choices,height=len(choices)+11) 778 | if code == d.OK and len(choices) > 0: 779 | nifs = [] 780 | 781 | for c in choices: 782 | nifs.append(shadow[c]) 783 | 784 | code = d.yesno("Are you sure you want to add the following interfaces:\n"+"\n".join(nifs)+"\nPrimary: "+nifs[0]) 785 | if code == d.OK: 786 | code, bondname = d.inputbox("Give your bond a name in the form bond e.g bond0",init="bond0") 787 | if code == d.OK: 788 | 789 | 790 | 791 | choices = [("1","active-backup"),("2","lacp")] 792 | 793 | code, modus = d.menu("Which modus do you want?",choices=choices,height=len(choices)+11) 794 | if code == d.OK: 795 | parameters = {"mode":"active-backup","primary":nifs[0],"mii-monitor-interval": 100} 796 | if modus == "2": 797 | parameters = {"mode":"802.3ad","lacp-rate":"fast","mii-monitor-interval":100} 798 | 799 | bonds = loadedyaml.get('network').get("bonds") 800 | if not bonds: 801 | bonds = {} 802 | 803 | pdef = loadedyaml.get('network').get("ethernets")[nifs[0]] 804 | bondconfig = {"dhcp4": True} 805 | 806 | if pdef: 807 | code = d.yesno("Do you want me to migrate the network from "+nifs[0]) 808 | if code == d.OK: 809 | bondconfig = pdef 810 | 811 | bondconfig["interfaces"] = nifs 812 | bondconfig["parameters"] = parameters 813 | bonds[bondname] = bondconfig 814 | for nif in nifs: 815 | loadedyaml.get('network').get('ethernets')[nif] = {'dhcp4': False} 816 | 817 | loadedyaml.get('network')["bonds"] = bonds 818 | newyaml = yaml.dump(loadedyaml) 819 | 820 | if newyaml != "": 821 | applynetplanyaml(config,d,yfile,newyaml) 822 | 823 | 824 | #6.4.4 825 | def managenetman(config,d): 826 | file = netfileselector(config,d) 827 | if file != "": 828 | openfile(config,d,file) 829 | 830 | code = d.yesno("\n".join(["Apply changes?"])) 831 | if code == d.OK: 832 | pout = subprocess.run(["netplan","apply"], capture_output=True) 833 | if pout.returncode != 0: 834 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 835 | #6.4.6 836 | def managebridge(config,d): 837 | yfile = netfileselector(config,d) 838 | if yfile != "": 839 | newyaml = "" 840 | with open(yfile,'r') as yamlfile: 841 | loadedyaml = yaml.load(yamlfile,Loader=yaml.SafeLoader) 842 | choices = [] 843 | shadow = {} 844 | ch = 1 845 | 846 | for net in veeamhubutil.realnics(): 847 | choices.append((str(ch),net,0)) 848 | shadow[str(ch)] = net 849 | ch = ch+1 850 | 851 | code,tag = d.menu("Select interfaces for your bridge",choices=choices,height=len(choices)+11) 852 | if code == d.OK: 853 | netif = shadow[tag] 854 | if d.yesno("Are you sure you want to create a bridge on "+netif) == d.OK: 855 | section = "ethernets" 856 | if netif in loadedyaml.get("network").get("bonds"): 857 | section = "bonds" 858 | 859 | 860 | brconfig = {"dhcp4": True} 861 | copysection = loadedyaml.get("network").get(section)[netif] 862 | 863 | updatetoorig = {"dhcp4": False} 864 | if copysection["interfaces"]: 865 | updatetoorig["interfaces"] = copysection["interfaces"] 866 | if copysection["parameters"]: 867 | updatetoorig["parameters"] = copysection["parameters"] 868 | loadedyaml.get("network").get(section)[netif] = updatetoorig 869 | 870 | if copysection: 871 | brconfig = copysection 872 | if brconfig["parameters"]: 873 | del brconfig["parameters"] 874 | brconfig["interfaces"] = [netif] 875 | 876 | brs = loadedyaml.get('network').get("bridges") 877 | if not brs: 878 | brs = {} 879 | 880 | brs["br0"] = brconfig 881 | 882 | loadedyaml.get('network')["bridges"] = brs 883 | newyaml = yaml.dump(loadedyaml) 884 | 885 | if newyaml != "": 886 | applynetplanyaml(config,d,yfile,newyaml) 887 | 888 | 889 | # 6.4 manage network 890 | def managenetwork(config,d): 891 | code = d.OK 892 | while code == d.OK: 893 | ln = [] 894 | ln.append("What do you want to do") 895 | code, tag = d.menu("\n".join(ln), 896 | choices=[("1", "Setup STATIC IP"), 897 | ("2", "Setup DHCP"), 898 | ("3", "Create a Bond"), 899 | ("4", "Manually change netplan"), 900 | ("5", "Create a bridge for LXD"), 901 | ],height=len(ln)+11) 902 | if code == d.OK: 903 | if tag == "1": 904 | managestaticip(config,d) 905 | elif tag == "2": 906 | managedhcp(config,d) 907 | elif tag == "3": 908 | managebond(config,d) 909 | elif tag == "4": 910 | managenetman(config,d) 911 | elif tag == "5": 912 | managebridge(config,d) 913 | 914 | # 6.5.1 lxdproxy 915 | def lxdsetup(config,d): 916 | code = d.yesno("\n".join(["Are you sure you want to set this up? This is highly experimental"])) 917 | if code == d.OK: 918 | pout = subprocess.run(["sudo","snap","install","lxd"], capture_output=True) 919 | if pout.returncode != 0: 920 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 921 | return 922 | else: 923 | if not "br0" in veeamhubutil.realnics(): 924 | d.msgbox("Please setup bridge networking first") 925 | else: 926 | d.infobox("Doing lxd initial setup, this can take a while") 927 | pout = subprocess.run(["lxd","init","--auto"], capture_output=True) 928 | if pout.returncode != 0: 929 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 930 | return 931 | pout = subprocess.run(["lxd","waitready"], capture_output=True) 932 | if pout.returncode != 0: 933 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 934 | return 935 | pout = subprocess.run("lxc profile copy default veeamproxy".split(), capture_output=True) 936 | if pout.returncode != 0: 937 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 938 | return 939 | pout = subprocess.run("lxc profile device remove veeamproxy eth0".split(), capture_output=True) 940 | if pout.returncode != 0: 941 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 942 | return 943 | 944 | 945 | def lxcproxyinfo(d,tolerateerror=False): 946 | s = subprocess.run(["lxc","list","lxdproxy","--format","yaml"], capture_output=True) 947 | lxpyaml = {"status":{"status_code":1}} 948 | 949 | if s.returncode == 0: 950 | lxpyaml = yaml.load(s.stdout,Loader=yaml.SafeLoader)[0] 951 | else: 952 | if not tolerateerror: 953 | d.msgbox("Unexpected error "+str(s.stderr)) 954 | return s.returncode,lxpyaml 955 | 956 | def lxdexec(d,step,instructarr): 957 | d.infobox("LXC exec : "+step) 958 | pout = subprocess.run(instructarr, capture_output=True) 959 | if pout.returncode != 0: 960 | d.msgbox("Error {0}".format(str(pout.stderr,'utf-8'))) 961 | return False 962 | else: 963 | return True 964 | 965 | def lxctryexec(d): 966 | trycmd = ["lxc","exec","lxdproxy","--","/bin/bash","-c","echo ''"] 967 | tryouts = 12 968 | 969 | pout = subprocess.run(trycmd, capture_output=True) 970 | while pout.returncode != 0 and tryouts > 0: 971 | pout = subprocess.run(trycmd, capture_output=True) 972 | tryouts = tryouts - 1 973 | d.infobox("Waiting for image to stabilize\n"+str(tryouts)+"\n"+str(pout.stderr)) 974 | time.sleep(5) 975 | 976 | if pout.returncode != 0: 977 | d.msgbox("Tried to stabilize image but didn't work "+str(pout.stderr)) 978 | return pout.returncode 979 | 980 | def lxdsetupproxy(config,d): 981 | steps = [ 982 | { "step":"lxd waitready", "instruct":["lxd","waitready"]}, 983 | { "step":"lxc proxy (download 3GB, get a coffee)", "instruct":["lxc","launch","ubuntu:20.04","lxdproxy","--vm","--network","br0","--profile","veeamproxy"] } 984 | ] 985 | for s in steps: 986 | success = lxdexec(d,s["step"],s["instruct"]) 987 | if not success: 988 | return 989 | 990 | #code 103 means the vm is running 991 | scode = 0 992 | while scode != 103 and scode != -1: 993 | err,lxcinfo = lxcproxyinfo(d,True) 994 | if err != 0: 995 | scode = -1 996 | scode = lxcinfo.get("state").get("status_code") 997 | d.infobox("Waiting for VM to get ready") 998 | time.sleep(1) 999 | 1000 | if scode == 103: 1001 | d.infobox("LXC proxy running") 1002 | lxctryexec(d) 1003 | 1004 | code,ipaddr = d.inputbox("IP for proxy","0.0.0.0/24") 1005 | code,gateway = d.inputbox("Gateway for proxy","0.0.0.0") 1006 | code,dns = d.inputbox("DNS server for proxy","8.8.8.8") 1007 | 1008 | #this really needs some cleaner way 1009 | modnet = 'NETPLAN=$(find /etc/netplan/*.yaml) && sed -i \'s/\\([ ]*\\)dhcp4: true/\\1addresses: ['+ipaddr.replace("/",'\\/')+']\\n\\1gateway4: '+gateway+'\\n\\1nameservers: { addresses: ['+dns+']}/\' $NETPLAN' 1010 | 1011 | steps = [ 1012 | { "step":"modifying NET", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c",modnet]}, 1013 | { "step":"applying NET", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c","netplan apply"]}, 1014 | { "step":"setup SSH", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c","ssh-keygen -A && systemctl start ssh && systemctl enable ssh"]}, 1015 | { "step":"setup USER", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c","useradd -m veeamproxy && echo 'veeamproxy:$6$moMETbaDQEN2m8pt$UTL5FzNLsIb1PbFGXSyr/AJiMjLJcYbcjxWyNEo7rTR5B04CCFxGatNArKOZvnL/UtnkhKLXJQH68y.O5a8sb.' | chpasswd -e"]}, 1016 | { "step":"allow PW Login", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c","sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config && systemctl restart sshd"]}, 1017 | { "step":"adding SUDO", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c","echo 'veeamproxy ALL=(ALL:ALL) ALL' >> /etc/sudoers"]}, 1018 | ] 1019 | #{ "step":"", "instruct":["lxc","exec","lxdproxy","--","/bin/bash","-c","''"]}, 1020 | for s in steps: 1021 | success = lxdexec(d,s["step"],s["instruct"]) 1022 | if not success: 1023 | return 1024 | 1025 | d.msgbox("Proxy should be reachable on "+ipaddr+" with veeamproxy:changeme\nMake sure to change the password by logging in via ssh/putty and using the passwd command") 1026 | 1027 | 1028 | 1029 | 1030 | def managelxd(config,d): 1031 | code,agree = d.inputbox("This potentially creates another security vector\nPlease type 'iunderstand' to continue") 1032 | if code == d.OK and agree == 'iunderstand': 1033 | while code == d.OK: 1034 | code, tag = d.menu("What do you want to do:", 1035 | choices=[("1", "Initial setup"), 1036 | ("2", "Create Proxy") 1037 | ]) 1038 | if code == d.OK: 1039 | if tag == "1": 1040 | lxdsetup(config,d) 1041 | elif tag == "2": 1042 | lxdsetupproxy(config,d) 1043 | 1044 | # 6 1045 | def manageubuntu(config,d): 1046 | code = d.OK 1047 | while code == d.OK: 1048 | code, tag = d.menu("What do you want to do:", 1049 | choices=[("1", "Update"), 1050 | ("2", "Harden"), 1051 | ("3", "Manage Time"), 1052 | ("4", "Manage Network"), 1053 | ("5", "Add Experimental LXD Proxy"), 1054 | ]) 1055 | if code == d.OK: 1056 | if tag == "1": 1057 | update(config,d) 1058 | elif tag == "2": 1059 | harden(config,d) 1060 | elif tag == "3": 1061 | managetime(config,d) 1062 | elif tag == "4": 1063 | managenetwork(config,d) 1064 | elif tag == "5": 1065 | managelxd(config,d) 1066 | 1067 | 1068 | # main loop 1069 | def saveconfig(cfile,config): 1070 | with open(cfile, 'w') as outfile: 1071 | json.dump(config, outfile) 1072 | 1073 | 1074 | def home(style="default"): 1075 | # http://pythondialog.sourceforge.net/ following this 1076 | # This is almost always a good thing to do at the beginning of your programs. 1077 | locale.setlocale(locale.LC_ALL, '') 1078 | 1079 | # config file is made under /etc/veeamhubtinyrepoman 1080 | # mainly keeps the repositories data and the repouser 1081 | cfile = Path('/etc') / "veeamhubtinyrepoman" 1082 | 1083 | #d = Dialog(dialog="dialog") 1084 | #d.set_background_title("VeeamHub Tiny Repo Manager") 1085 | 1086 | rows,columns = dialogwrappers.screensize() 1087 | 1088 | if (rows < 40 or columns < 90) and style == "default": 1089 | print("Switching to alternate dialog style because small terminal (r{},c{}), need at least 40 rows and 90 columns".format(rows,columns)) 1090 | print("To avoid this message, resize the terminal or run with -alt flag") 1091 | print("In VMware the console screen might be to small, you can adapt it with vga=791 in grub") 1092 | print("https://askubuntu.com/questions/86561/how-can-i-increase-the-console-resolution-of-my-ubuntu-server") 1093 | 1094 | style="alternate" 1095 | time.sleep(2) 1096 | 1097 | 1098 | d = 0 1099 | if style == "alternate": 1100 | d = dialogwrappers.AlternateDialog("Alternate VeeamHub Tiny Repo Manager",rows,columns) 1101 | else: 1102 | d = dialogwrappers.DialogWrapper("VeeamHub Tiny Repo Manager") 1103 | 1104 | 1105 | code = d.OK 1106 | config = {"repouser":"veeamrepo","vbrserver":"","reader":["nano","-v"],"writer":["nano"],"registertimeout":500} 1107 | 1108 | firstrun = False 1109 | 1110 | # json file. If it does not exists, it's create with the default settings above 1111 | # if exists, it is read 1112 | if not cfile.is_file(): 1113 | d.infobox("Trying to create:\n{}".format(str(cfile)),width=80) 1114 | time.sleep(2) 1115 | firstrun = True 1116 | config['repositories'] = [] 1117 | with open(cfile, 'w') as outfile: 1118 | json.dump(config, outfile) 1119 | else: 1120 | with open(cfile, 'r') as outfile: 1121 | config = json.load(outfile) 1122 | 1123 | 1124 | if firstrun: 1125 | c = d.yesno("This is the first time you started veeamhubrepo\n\nDo you want to run the wizard process?\n\nThis will execute certain actions automatically!",width=60,height=15) 1126 | if c == d.OK: 1127 | setrepouser(config,d) 1128 | saveconfig(cfile,config) 1129 | 1130 | rcode,mp = formatdrive(config,d) 1131 | if rcode == 0 and mp != "": 1132 | config['repositories'].append(mp) 1133 | saveconfig(cfile,config) 1134 | 1135 | c = d.yesno("Do you want to configure NTP and the timezone?") 1136 | if c == d.OK: 1137 | configtimezone(config,d) 1138 | ntp(config,d) 1139 | 1140 | d.infobox("Disabling SSH at startup") 1141 | time.sleep(1) 1142 | disablessh() 1143 | 1144 | d.infobox("Enabling the firewall") 1145 | time.sleep(1) 1146 | enablefw() 1147 | 1148 | c = d.yesno("Do you want to try to update the server now?") 1149 | if c == d.OK: 1150 | update(config,d) 1151 | 1152 | c = d.yesno("Are you ready to register the server with Veeam B&R now?") 1153 | if c == d.OK: 1154 | registerserver(config,d,True) 1155 | 1156 | 1157 | 1158 | # while you keep getting ok, keep going 1159 | while code == d.OK: 1160 | updated = False 1161 | 1162 | ln = ["Current IPv4: {}".format(",".join(veeamhubutil.myips()))] 1163 | if veeamhubutil.is_ssh_on(): 1164 | ln.append("! SSH is running !") 1165 | 1166 | ln.append("") 1167 | ln.append("What do you want to do:") 1168 | 1169 | # keep structure as it, add new functionality under sub menu so that it doesn't get too big 1170 | code, tag = d.menu("\n".join(ln), 1171 | choices=[("1", "Set/Create Unprivileged Repo User"), 1172 | ("2", "Format Drive XFS"), 1173 | ("3", "Register Hardened Repo"), 1174 | ("4", "Monitor Repositories"), 1175 | ("5", "Manages Repo Paths"), 1176 | ("6", "Manage Ubuntu"), 1177 | ],height=len(ln)+14,cancel="Exit") 1178 | if code == d.OK: 1179 | if tag == "1": 1180 | setrepouser(config,d) 1181 | updated = True 1182 | elif tag == "2" or tag == "5": 1183 | if veeamhubutil.usersexists(config["repouser"]): 1184 | if tag == "2": 1185 | rcode,mp = formatdrive(config,d) 1186 | if rcode == 0 and mp != "": 1187 | config['repositories'].append(mp) 1188 | updated = True 1189 | elif tag == "5": 1190 | updated = managerepo(config,d) 1191 | else: 1192 | d.msgbox("Please create repo user first") 1193 | elif tag == "3": 1194 | registerserver(config,d) 1195 | elif tag == "4": 1196 | monitorrepos(config,d) 1197 | elif tag == "6": 1198 | manageubuntu(config,d) 1199 | if updated: 1200 | with open(cfile, 'w') as outfile: 1201 | json.dump(config, outfile) 1202 | 1203 | # cleans output after being done 1204 | def main(): 1205 | args = sys.argv[0:] 1206 | if "-alt" in args: 1207 | home(style="alternate") 1208 | else: 1209 | home(style="default") 1210 | 1211 | subprocess.run(["clear"]) 1212 | 1213 | if __name__ == "__main__": 1214 | if os.getuid() != 0: 1215 | print("You are running this command as a regular user") 1216 | print("Please use sudo e.g. sudo veeamhubrepo") 1217 | exit(1) 1218 | main() 1219 | -------------------------------------------------------------------------------- /src/veeamhubutil.py: -------------------------------------------------------------------------------- 1 | # 2 | # non dialog functions 3 | 4 | import netifaces 5 | import psutil 6 | import pystemd 7 | import subprocess 8 | import re 9 | import json 10 | import humanize 11 | 12 | # creates a list of all ipv4 address 13 | def realnics(): 14 | return [i for i in netifaces.interfaces() if not i == 'lo' ] 15 | 16 | def myips(): 17 | ipaddr = [] 18 | for nif in realnics(): 19 | ifaddr = netifaces.ifaddresses(nif) 20 | if netifaces.AF_INET in ifaddr: 21 | addrdetails = ifaddr[netifaces.AF_INET] 22 | for addr in addrdetails: 23 | ip = addr['addr'] 24 | if ip != '127.0.0.1': 25 | ipaddr.append(ip) 26 | return ipaddr 27 | 28 | 29 | def firstipwithnet(networkinterface): 30 | ipwithnet = "169.254.128.128/24" 31 | 32 | if netifaces.AF_INET in netifaces.ifaddresses(networkinterface): 33 | addrs = netifaces.ifaddresses(networkinterface)[netifaces.AF_INET] 34 | if len(addrs) > 0: 35 | nm = ipaddress.IPv4Network("0.0.0.0/"+addrs[0]['netmask']) 36 | ipwithnet = addrs[0]['addr'] + "/" + str(nm.prefixlen) 37 | 38 | return ipwithnet 39 | 40 | def firstgw(networkinterface): 41 | gw = "169.254.128.1" 42 | 43 | gws = netifaces.gateways() 44 | if netifaces.AF_INET in gws: 45 | fgw = [g for g in gws[netifaces.AF_INET] if g[1] == networkinterface ] 46 | if len(fgw) > 0: 47 | gw = fgw[0][0] 48 | 49 | return gw 50 | 51 | 52 | def veeamrunning(): 53 | procs = [p for p in psutil.process_iter(['pid', 'name', 'username']) if "veeamtransport" in p.name()] 54 | return len(procs) > 0 55 | 56 | def veeamreposshcheck(username): 57 | procs = [p for p in psutil.process_iter(['pid', 'name', 'username']) if username in p.username() and "ssh" in p.name()] 58 | return len(procs) > 0 59 | 60 | def getsshservice(): 61 | ssh = pystemd.systemd1.Unit(b'ssh.service') 62 | ssh.load() 63 | return ssh 64 | 65 | def is_ssh_on(): 66 | s = getsshservice() 67 | return s.Unit.ActiveState == b'active' 68 | 69 | def ufw_is_inactive(): 70 | ufw = subprocess.run(["ufw", "status"], capture_output=True) 71 | if ufw.returncode == 0: 72 | return "inactive" in str(ufw.stdout,"utf-8") 73 | else: 74 | raise Exception("ufw feedback not as expected {}".format(ufw.stdout)) 75 | 76 | def ufw_activate(): 77 | ufw = subprocess.run(["ufw","--force","enable"], capture_output=True) 78 | if ufw.returncode != 0: 79 | raise Exception("ufw feedback not as expected {}".format(ufw.stdout)) 80 | else: 81 | return ufw.stdout 82 | 83 | def ufw_ssh(setstatus="deny"): 84 | ufw = subprocess.run(["ufw",setstatus,"ssh"], capture_output=True) 85 | if ufw.returncode != 0: 86 | raise Exception("ufw feedback not as expected {}".format(ufw.stdout)) 87 | else: 88 | return ufw.stdout 89 | 90 | 91 | def gettimeinfo(): 92 | timeinfo = ["Could not fetch timeinfo"] 93 | time = "" 94 | date = "" 95 | zone = "" 96 | ntpactive = False 97 | pout = subprocess.run(["timedatectl","status"], capture_output=True) 98 | if pout.returncode == 0: 99 | timeinfo = [] 100 | for line in str(pout.stdout,'utf-8').split("\n"): 101 | line = line.strip() 102 | if line != "": 103 | tim = re.match("Local time: [A-Za-z]+ ([0-9]{4}-[0-9]{2}-[0-9]{2}) ([0-9]{2}:[0-9]{2}:[0-9]{2})",line) 104 | if tim: 105 | time = tim.group(2) 106 | date = tim.group(1) 107 | else: 108 | zm = re.match("Time zone: ([A-Za-z/]+)",line) 109 | if zm: 110 | zone = zm.group(1) 111 | else: 112 | ntpm = re.match("NTP service: ([A-Za-z/]+)",line) 113 | if ntpm: 114 | ntpactive = ntpm.group(1) == "active" 115 | 116 | timeinfo.append(line) 117 | 118 | return timeinfo,time,date,zone,ntpactive 119 | 120 | def packagetest(dpkgtest): 121 | code = 0 122 | pout = subprocess.run(["dpkg","-s",dpkgtest], capture_output=True) 123 | if pout.returncode != 0: 124 | code = -1 125 | else: 126 | for ln in str(pout.stdout,"utf-8").split("\n"): 127 | if re.match("Status: install ok installed",ln): 128 | code = 1 129 | return code 130 | 131 | def removepackage(packagename): 132 | pout = subprocess.run(["apt-get","remove",packagename,"-y"], capture_output=True) 133 | if pout.returncode != 0: 134 | raise Exception("Error removing {0}".format(str(pout.stderr,'utf-8'))) 135 | 136 | 137 | 138 | def installpackage(d,packagename): 139 | pout = subprocess.run(["apt-get","update","-y"], capture_output=True) 140 | if pout.returncode != 0: 141 | raise Exception("Error updating {0}".format(str(pout.stderr,'utf-8'))) 142 | 143 | pout = subprocess.run(["apt-get","install",packagename,"-y"], capture_output=True) 144 | if pout.returncode != 0: 145 | raise Exception("Error updating {0}".format(str(pout.stderr,'utf-8'))) 146 | 147 | 148 | def usersexists(user): 149 | found = False 150 | with open("/etc/passwd", 'r') as outfile: 151 | allu = outfile.readlines() 152 | for u in allu: 153 | us = u.split(":") 154 | if us[0] == user: 155 | found = True 156 | 157 | return found 158 | 159 | # Makes a list of candidate drives from lsblk 160 | # Uses recursion to dig deeper 161 | # Drive is candidate if it does not have child partitions / is not mounter / is not a CD 162 | # In case of children, do the recursion 163 | # I is kept to keep the logical order 164 | #recursive lsblk() 165 | class BlkPathSize: 166 | Path = "" 167 | Size = 0 168 | 169 | def MenuEntry(self): 170 | return "{} {}".format(self.Path,humanize.filesize.naturalsize(self.Size)) 171 | 172 | def __init__(self,jsonblock): 173 | self.Path = jsonblock["path"] 174 | self.Size = jsonblock["size"] 175 | 176 | def rlsblk(blkdevices,blklist): 177 | for device in blkdevices: 178 | if not device["mountpoint"] and not "children" in device and not device["maj:min"].split(":")[0] == "11" : 179 | #11 -> /dev/sr[] 180 | blkdev = BlkPathSize(device) 181 | #if bigger then 1GB, cause everything smaller than 1GB really doesn't make sense 182 | if blkdev.Size > 1073741824: 183 | blklist.append(blkdev) 184 | 185 | elif "children" in device: 186 | rlsblk(device["children"],blklist) 187 | 188 | 189 | def lsblk(): 190 | lsout = subprocess.run(["lsblk", "--json","-b","-o","PATH,MAJ:MIN,NAME,MOUNTPOINT,SIZE"], capture_output=True) 191 | if lsout.returncode != 0: 192 | raise Exception("Unable to load partition schema"+lsout.stderr) 193 | 194 | jout = json.loads(lsout.stdout) 195 | blklist = [] 196 | rlsblk(jout["blockdevices"],blklist) 197 | return blklist 198 | 199 | 200 | 201 | 202 | 203 | --------------------------------------------------------------------------------