├── .bumpversion.cfg ├── .editorconfig ├── .gitignore ├── .mailmap ├── .travis.yml ├── LICENSE ├── Makefile ├── README-windows.md ├── README.md ├── VERSION ├── __init__.py ├── bin ├── parts-cluster.py └── parts.py ├── cloudmesh ├── __init__.py └── burn │ ├── Imager.py │ ├── Iso.py │ ├── __init__.py │ ├── __version__.py │ ├── burner │ ├── Burner.py │ ├── BurnerABC.py │ ├── RaspberryBurner.py │ ├── __init__.py │ ├── raspberryos.py │ └── ubuntu.py │ ├── command │ ├── __init__.py │ └── burn.py │ ├── gui.py │ ├── hardware.py │ ├── image.py │ ├── images │ ├── __init__.py │ ├── cm-logo-100.png │ ├── cm-logo.png │ ├── raspberry-logo-black.png │ ├── raspberry-logo-white-100.png │ └── raspberry-logo-white.png │ ├── network.py │ ├── raspberryos │ ├── Locale.py │ ├── __init__.py │ ├── cmdline.py │ ├── passwd.py │ ├── runfirst.py │ └── test.py │ ├── sdcard.py │ ├── ubuntu │ ├── __init__.py │ ├── configure.py │ ├── networkdata.py │ └── userdata.py │ ├── usb.py │ ├── util.py │ ├── wifi │ ├── __init__.py │ ├── network-config │ ├── provider.py │ ├── raspberryos.py │ ├── ssid.py │ └── ubuntu.py │ └── windowssdcard.py ├── cm-pi-burn.md ├── deprecated ├── README-02-18-2021.md ├── README-hackaday.md ├── README-notes.md ├── README-old.md ├── activate_ssh.py ├── activate_ssh.py-removed ├── admin.py-removed ├── card │ └── pi.py ├── cloudinit.py ├── cmburn.py-removed ├── detect.py-removed ├── dmesg-usb ├── info.py ├── parts.py ├── raspberryos16.py ├── report-reu.md └── staticip ├── dev-scripts ├── TestNotebook.py ├── a.sh ├── ccc.py ├── ddd.py ├── ddd1.py ├── g.py ├── ggg.py ├── l.py ├── m.py ├── notes.md ├── p.py ├── q.py ├── test2.txt └── x.py ├── docs ├── README.md └── vonLaszewski-cmburn.pdf ├── examples ├── example_image_clear.py └── gregor.py ├── experimental ├── cm-logo.png ├── fake.py ├── gui.py ├── progress.py ├── screenshoot.png └── sdcard-burn-pi-headless │ ├── featured-many-pis.jpg │ ├── index.md │ └── many-pis.jpg ├── images ├── hostname1.png ├── hostname2.png ├── imager-with-options.png ├── imager.png ├── mesh-pi.pdf ├── mesh-pi.png ├── network-bridge.png ├── network-cluster.png ├── network-keys.png ├── network-mesh.png ├── network.png ├── network.pptx ├── pi_clusters.jpg ├── pi_clusters_case.jpg └── sdcc.png ├── paper ├── IEEEtran.cls ├── Makefile ├── README.md ├── bibliography.bib ├── bibliography.csl ├── bin │ ├── includes.hs │ └── table-filter.py ├── manual.md ├── metadata.yaml ├── pandoc-crossref.yaml ├── requirements.txt ├── template.latex └── vonLaszewski-cmburn.md ├── pytest.ini ├── requirements-dev.txt ├── requirements-win.txt ├── requirements-windows.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── README-testing.md ├── hosts ├── results.md ├── test_01_image.py ├── test_02_burn.py ├── test_03_clone.py ├── test_04_networkdata.py ├── test_05_userdata.py └── test_06_configure.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.3.30 3 | commit = True 4 | tag = False 5 | 6 | [bumpversion:file:VERSION] 7 | 8 | [bumpversion:file:./cloudmesh/burn/__version__.py] 9 | 10 | [bumpversion:file:./cloudmesh/burn/__init__.py] 11 | search = version: {current_version} 12 | replace = {new_version} 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [*.{js, py}] 14 | charset = utf-8 15 | 16 | # 4 space indentation 17 | [*.py] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | # Tab indentation (no size specified) 22 | [Makefile] 23 | indent_style = tab 24 | 25 | # Indentation override for all JS under lib directory 26 | [lib/**.js] 27 | indent_style = space 28 | indent_size = 2 29 | 30 | # Matches the exact files either package.json or .travis.yml 31 | [{package.json, .travis.yml}] 32 | indent_style = space 33 | indent_size = 2 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Makefilee 3 | *.pye 4 | .idea 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Gregor von Laszewski 2 | Richard Otten 3 | Subramaniyam Raizada 4 | Anand Sriramulu 5 | Richard Otten 6 | Richard Otten <32284386+rickotten@users.noreply.github.com> 7 | Akshay Kowshik <54605176+akshayrf12345@users.noreply.github.com> 8 | Jonathan Branam 9 | Anand Sriramulu 10 | Daivik Dayanand <54679000+Daivik1997@users.noreply.github.com> 11 | Fugang Wang 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: 5 | - pip install -r requirements.txt 6 | - pip install -r requirements-dev.txt 7 | script: 8 | - flake8 --max-line-length 124 --ignore=E722 cloudmesh 9 | - flake8 --max-line-length 124 --ignore=E722 tests 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | Copyright 2017 Gregor von Laszewski, Indiana University 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | package=pi-burn 2 | UNAME=$(shell uname) 3 | VERSION=`head -1 VERSION` 4 | 5 | define banner 6 | @echo 7 | @echo "###################################" 8 | @echo $(1) 9 | @echo "###################################" 10 | endef 11 | 12 | all: install 13 | 14 | flake8: 15 | cd ..; flake8 --max-line-length 124 --ignore=E722 cloudmesh-$(package)/cloudmesh 16 | cd ..; flake8 --max-line-length 124 --ignore=E722 cloudmesh-$(package)/tests 17 | 18 | install: 19 | pip install -e . 20 | 21 | readme: 22 | cms markdown -p numbers ./README.md 23 | cms man readme -p --toc 24 | cms man readme -p --tag="MANUAL-BURN" --command=burn 25 | cms man readme -p --tag="MANUAL-BRIDGE" --command=bridge 26 | cms man readme -p --tag="MANUAL-HOST" --command=host 27 | cms man readme -p --tag="MANUAL-PI" --command=pi 28 | cms man readme -p --tag="MANUAL-SSH" --command=ssh 29 | 30 | source: 31 | cd ../cloudmesh.cmd5; make source 32 | $(call banner, "Install cloudmesh-{package}") 33 | pip install -e . -U 34 | cms help 35 | 36 | #requirements: 37 | # echo "cloudmesh-cmd5" > tmp.txt 38 | # echo "cloudmesh-sys" >> tmp.txt 39 | # echo "cloudmesh-inventory" >> tmp.txt 40 | # echo "cloudmesh-configuration" >> tmp.txt 41 | # # pip-compile setup.py 42 | # #fgrep -v "# via" requirements.txt | fgrep -v "cloudmesh" >> tmp.txt 43 | # mv tmp.txt requirements.txt 44 | # -git commit -m "update requirements" requirements.txt 45 | # -git push 46 | 47 | manual: 48 | mkdir -p docs-source/source/manual 49 | cms help > /tmp/commands.rst 50 | echo "Commands" > docs-source/source/manual/commands.rst 51 | echo "========" >> docs-source/source/manual/commands.rst 52 | echo >> docs-source/source/manual/commands.rst 53 | tail -n +4 /tmp/commands.rst >> docs-source/source/manual/commands.rst 54 | cms man --kind=rst burn > docs-source/source/manual/admin.rst 55 | cms man --kind=rst foo > docs-source/source/manual/banner.rst 56 | 57 | doc: 58 | rm -rf docs 59 | mkdir -p dest 60 | cd docs-source; make html 61 | cp -r docs-source/build/html/ docs 62 | 63 | view: 64 | open docs/index.html 65 | 66 | # 67 | # TODO: BUG: This is broken 68 | # 69 | #pylint: 70 | # mkdir -p docs/qc/pylint/cm 71 | # pylint --output-format=html cloudmesh > docs/qc/pylint/cm/cloudmesh.html 72 | # pylint --output-format=html cloud > docs/qc/pylint/cm/cloud.html 73 | 74 | clean: 75 | $(call banner, "CLEAN") 76 | rm -rf dist 77 | rm -rf *.zip 78 | rm -rf *.egg-info 79 | rm -rf *.eggs 80 | rm -rf docs/build 81 | rm -rf build 82 | find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf 83 | rm -rf .tox 84 | rm -f *.whl 85 | 86 | ###################################################################### 87 | # PYPI 88 | ###################################################################### 89 | 90 | 91 | twine: 92 | pip install -U twine 93 | 94 | dist: 95 | python setup.py sdist bdist_wheel 96 | twine check dist/* 97 | 98 | patch: clean 99 | $(call banner, "bbuild") 100 | bump2version --no-tag --allow-dirty patch 101 | python setup.py sdist bdist_wheel 102 | git push 103 | # git push origin master --tags 104 | twine check dist/* 105 | twine upload --repository testpypi dist/* 106 | # $(call banner, "install") 107 | # sleep 10 108 | # pip install --index-url https://test.pypi.org/simple/ cloudmesh-$(package) -U 109 | 110 | #make 111 | #git commit -m "update documentation" docs 112 | #git push 113 | 114 | minor: clean 115 | $(call banner, "minor") 116 | bump2version minor --allow-dirty 117 | @cat VERSION 118 | @echo 119 | 120 | release: clean 121 | $(call banner, "release") 122 | -git tag "v$(VERSION)" 123 | -git push origin master --tags 124 | python setup.py sdist bdist_wheel 125 | twine check dist/* 126 | twine upload --repository pypi dist/* 127 | $(call banner, "install") 128 | @cat VERSION 129 | @echo 130 | #sleep 10 131 | #pip install -U cloudmesh-common 132 | 133 | 134 | dev: 135 | bump2version --new-version "$(VERSION)-dev0" part --allow-dirty 136 | bump2version patch --allow-dirty 137 | @cat VERSION 138 | @echo 139 | 140 | reset: 141 | bump2version --new-version "4.0.0-dev0" part --allow-dirty 142 | 143 | upload: 144 | twine check dist/* 145 | twine upload dist/* 146 | 147 | pip: 148 | pip install --index-url https://test.pypi.org/simple/ cloudmesh-$(package) -U 149 | 150 | # --extra-index-url https://test.pypi.org/simple 151 | 152 | log: 153 | $(call banner, log) 154 | gitchangelog | fgrep -v ":dev:" | fgrep -v ":new:" > ChangeLog 155 | git commit -m "chg: dev: Update ChangeLog" ChangeLog 156 | git push 157 | 158 | 159 | -------------------------------------------------------------------------------- /README-windows.md: -------------------------------------------------------------------------------- 1 | ``` 2 | DISKPART> LIST VOLUME 3 | 4 | Volume ### Ltr Label Fs Type Size Status Info 5 | ---------- --- ----------- ----- ---------- ------- --------- -------- 6 | Volume 0 F DVD-ROM 0 B No Media 7 | Volume 1 BDEDrive NTFS Partition 1024 MB Healthy 8 | Volume 2 C OSDisk NTFS Partition 463 GB Healthy Boot 9 | Volume 3 FAT32 Partition 512 MB Healthy System 10 | Volume 4 NTFS Partition 496 MB Healthy Hidden 11 | Volume 5 D UNTITLED exFAT Removable 59 GB Healthy 12 | 13 | DISKPART> list disk 14 | 15 | Disk ### Status Size Free Dyn Gpt 16 | -------- ------------- ------- ------- --- --- 17 | Disk 0 Online 465 GB 1024 KB * 18 | Disk 1 Online 59 GB 0 B 19 | 20 | DISKPART> list volume 21 | 22 | Volume ### Ltr Label Fs Type Size Status Info 23 | ---------- --- ----------- ----- ---------- ------- --------- -------- 24 | Volume 0 F DVD-ROM 0 B No Media 25 | Volume 1 BDEDrive NTFS Partition 1024 MB Healthy 26 | Volume 2 C OSDisk NTFS Partition 463 GB Healthy Boot 27 | Volume 3 FAT32 Partition 512 MB Healthy System 28 | Volume 4 NTFS Partition 496 MB Healthy Hidden 29 | Volume 5 D system-boot FAT32 Removable 256 MB Healthy 30 | 31 | DISKPART> list disk 32 | 33 | Disk ### Status Size Free Dyn Gpt 34 | -------- ------------- ------- ------- --- --- 35 | Disk 0 Online 465 GB 1024 KB * 36 | Disk 1 Online 59 GB 0 B 37 | 38 | DISKPART> list volume 39 | 40 | Volume ### Ltr Label Fs Type Size Status Info 41 | ---------- --- ----------- ----- ---------- ------- --------- -------- 42 | Volume 0 F DVD-ROM 0 B No Media 43 | Volume 1 BDEDrive NTFS Partition 1024 MB Healthy 44 | Volume 2 C OSDisk NTFS Partition 463 GB Healthy Boot 45 | Volume 3 FAT32 Partition 512 MB Healthy System 46 | Volume 4 NTFS Partition 496 MB Healthy Hidden 47 | Volume 5 D boot FAT32 Removable 256 MB Healthy 48 | 49 | DISKPART> list disk 50 | 51 | Disk ### Status Size Free Dyn Gpt 52 | -------- ------------- ------- ------- --- --- 53 | Disk 0 Online 465 GB 1024 KB * 54 | Disk 1 Online 59 GB 51 GB 55 | 56 | DISKPART> 57 | 58 | 59 | DISKPART> select disk 3 60 | 61 | Disk 3 is now the selected disk. 62 | 63 | DISKPART> select volume 5 64 | 65 | Volume 5 is the selected volume. 66 | 67 | DISKPART> format fs=fat32 quick 68 | 69 | 100 percent completed 70 | 71 | DiskPart successfully formatted the volume. 72 | 73 | DISKPART> list disk 74 | 75 | Disk ### Status Size Free Dyn Gpt 76 | -------- ------------- ------- ------- --- --- 77 | Disk 0 Online 931 GB 12 MB * 78 | Disk 1 No Media 0 B 0 B 79 | Disk 2 No Media 0 B 0 B 80 | * Disk 3 Online 59 GB 3072 KB 81 | 82 | DISKPART> list disk volume 83 | 84 | The arguments specified for this command are not valid. 85 | For more information on the command type: HELP LIST DISK 86 | 87 | DISKPART> list volume 88 | 89 | Volume ### Ltr Label Fs Type Size Status Info 90 | ---------- --- ----------- ----- ---------- ------- --------- -------- 91 | Volume 0 C OS NTFS Partition 930 GB Healthy Boot 92 | Volume 1 ESP FAT32 Partition 500 MB Healthy System 93 | Volume 2 NTFS Partition 851 MB Healthy Hidden 94 | Volume 3 D Removable 0 B No Media 95 | Volume 4 E Removable 0 B No Media 96 | * Volume 5 F FAT32 Removable 256 MB Healthy 97 | 98 | ``` 99 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 4.3.30 2 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/__init__.py -------------------------------------------------------------------------------- /bin/parts-cluster.py: -------------------------------------------------------------------------------- 1 | import oyaml as yaml 2 | import sys 3 | from cloudmesh.common.util import readfile 4 | import pandas as pd 5 | 6 | order = ["vendor", 7 | "description", 8 | "included", 9 | "price", 10 | "count", 11 | "total", 12 | "comment", 13 | "image"] 14 | 15 | pd.options.display.float_format = '{:,.2f}'.format 16 | 17 | file = sys.argv[1] # yaml file 18 | 19 | content = readfile(file) 20 | 21 | content = yaml.safe_load(content) 22 | 23 | 24 | data = {} 25 | for i in range(0,len(content)): 26 | description = content[i]["description"] 27 | url = content[i]["url"] 28 | content[i]["link"] = f"[{description}]({url})" 29 | data[i] = content[i] 30 | image_url = content[i]["image"] 31 | content[i]["image"] = f"![]({image_url})" 32 | 33 | 34 | df = pd.DataFrame( 35 | data=data, 36 | index=["vendor", 37 | "description", 38 | "included", 39 | "price", 40 | "comment", 41 | "other", 42 | "url", 43 | "link", 44 | "image", 45 | "count"] 46 | ).transpose() 47 | 48 | df['description'] = df['link'] 49 | 50 | 51 | df["total"] = df["count"] * df["price"] 52 | total = round(df["total"].sum(),2) 53 | 54 | df = df.round(2) 55 | 56 | 57 | df = df[order] 58 | 59 | entry = {} 60 | for a in order: 61 | entry[a] = "" 62 | 63 | 64 | entry["total"] = " ======== " 65 | df = df.append(entry, ignore_index=True) 66 | 67 | entry["total"] = total 68 | df = df.append(entry, ignore_index=True) 69 | 70 | table = df.to_markdown() 71 | 72 | 73 | print() 74 | print (table) 75 | print() 76 | -------------------------------------------------------------------------------- /bin/parts.py: -------------------------------------------------------------------------------- 1 | import oyaml as yaml 2 | import sys 3 | from cloudmesh.common.util import readfile 4 | import pandas as pd 5 | 6 | order = ["vendor", 7 | "description", 8 | "included", 9 | "price", 10 | "comment", 11 | "image"] 12 | 13 | pd.options.display.float_format = '{:,.2f}'.format 14 | 15 | try: 16 | file = sys.argv[1] 17 | except: 18 | file = "README-parts-list.yml" 19 | 20 | content = readfile(file) 21 | 22 | content = yaml.safe_load(content) 23 | 24 | 25 | data = {} 26 | for i in range(0,len(content)): 27 | description = content[i]["description"] 28 | url = content[i]["url"] 29 | content[i]["link"] = f"[{description}]({url})" 30 | data[i] = content[i] 31 | image_url = content[i]["image"] 32 | content[i]["image"] = f"![]({image_url})" 33 | 34 | df = pd.DataFrame( 35 | data=data, 36 | index=["vendor", 37 | "description", 38 | "included", 39 | "price", 40 | "comment", 41 | "other", 42 | "url", 43 | "link", 44 | "image"] 45 | 46 | ).transpose() 47 | 48 | df['description'] = df['link'] 49 | 50 | 51 | #df["total"] = df["count"] * df["price"] 52 | #total = round(df["total"].sum(),2) 53 | df = df.round(2) 54 | 55 | 56 | df = df[order] 57 | 58 | entry = {} 59 | for a in order: 60 | entry[a] = "" 61 | 62 | 63 | #entry["total"] = " ======== " 64 | #df = df.append(entry, ignore_index=True) 65 | 66 | #entry["total"] = total 67 | #df = df.append(entry, ignore_index=True) 68 | 69 | table = df.to_markdown() 70 | 71 | 72 | print() 73 | print (table) 74 | print() 75 | -------------------------------------------------------------------------------- /cloudmesh/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | pkg_resources.declare_namespace(__name__) 4 | -------------------------------------------------------------------------------- /cloudmesh/burn/Imager.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cloudmesh.common.Shell import Shell 3 | 4 | from cloudmesh.burn.image import Image 5 | from cloudmesh.common.systeminfo import os_is_linux 6 | from cloudmesh.common.systeminfo import os_is_pi 7 | from cloudmesh.common.systeminfo import os_is_mac 8 | from cloudmesh.common.systeminfo import os_is_windows 9 | from cloudmesh.common.console import Console 10 | from cloudmesh.common.sudo import Sudo 11 | 12 | 13 | class Imager: 14 | 15 | @staticmethod 16 | def installed(): 17 | r = Shell.which("rpi-imager") 18 | return r is not None 19 | 20 | @staticmethod 21 | def install(force=False): 22 | if os_is_mac(): 23 | return 24 | if not Imager.installed() or force: 25 | if os_is_linux() or os_is_pi(): 26 | Sudo.password() 27 | os.system("sudo apt uninstall -y rpi-imager") 28 | else: 29 | Console.warning("Installation is not supported") 30 | 31 | @staticmethod 32 | def fetch(tag=None): 33 | tag = tag or ['latest-lite'] 34 | Image.create_version_cache() 35 | file = Image().fetch(tag=tag) 36 | 37 | return file 38 | 39 | @staticmethod 40 | def launch(file=None): 41 | 42 | if file is not None: 43 | 44 | if not str(file).endswith(".img"): 45 | raise ValueError(f"file {file} does not end with .img") 46 | 47 | if not os.path.exists(file): 48 | raise ValueError(f"image file {file} does not exist") 49 | 50 | elif file is None: 51 | file = "" 52 | 53 | Imager.install() 54 | 55 | if os_is_linux() or os_is_pi(): 56 | Sudo.password() 57 | os.system(f"sudo rpi-imager {file}") 58 | elif os_is_mac(): 59 | os.system(f"/Applications/Raspberry\ Pi\ Imager.app/Contents/MacOS/rpi-imager {file} " # noqa: W605 60 | "> /dev/null 2>&1") # noqa: W605 61 | elif os_is_windows(): 62 | os.chdir(r"C:\Program Files (x86)\Raspberry Pi Imager") 63 | os.system("rpi-imager.exe") 64 | -------------------------------------------------------------------------------- /cloudmesh/burn/Iso.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cloudmesh.burn.image import Image 4 | 5 | 6 | class Iso: 7 | distribution = { 8 | "latest": "https://downloads.raspberrypi.org/rpd_x86/images/rpd_x86-2021-01-12/2021-01-11-raspios-buster-i386.iso" # noqa: E501 9 | } 10 | 11 | @staticmethod 12 | def get(tag="latest"): 13 | url = Iso.distribution[tag] 14 | destination = Image().directory + os.path.basename(url) 15 | 16 | os.system(f'wget -O {destination} {url}') 17 | -------------------------------------------------------------------------------- /cloudmesh/burn/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "4.3.30" 2 | -------------------------------------------------------------------------------- /cloudmesh/burn/__version__.py: -------------------------------------------------------------------------------- 1 | version = "4.3.30" 2 | -------------------------------------------------------------------------------- /cloudmesh/burn/burner/Burner.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from cloudmesh.burn.burner.raspberryos import Burner as RaspberryOsBurner 5 | from cloudmesh.burn.usb import USB 6 | from cloudmesh.common.systeminfo import os_is_linux 7 | from cloudmesh.common.systeminfo import os_is_mac 8 | from cloudmesh.common.systeminfo import os_is_pi 9 | from cloudmesh.common.systeminfo import os_is_windows 10 | from cloudmesh.common.JobScript import JobScript 11 | from cloudmesh.common.Tabulate import Printer 12 | from cloudmesh.common.console import Console 13 | from cloudmesh.common.util import banner 14 | from cloudmesh.common.util import path_expand 15 | 16 | 17 | # class Burner(AbstractBurner): 18 | class Burner: 19 | 20 | def __init__(self, card_os="raspberryos"): 21 | if "raspberry" in card_os: 22 | self.burner = RaspberryOsBurner() 23 | else: 24 | Console.error("Card OS not supported") 25 | 26 | def get(self): 27 | return self.burner 28 | 29 | @staticmethod 30 | def detect(): 31 | if os_is_mac(): 32 | details = USB.get_from_diskutil() 33 | elif os_is_windows(): 34 | Console.error("detect: Windws is not yet supported") 35 | sys.exit() 36 | else: 37 | details = USB.get_from_dmesg() 38 | return details 39 | 40 | def shrink(self, image=None): 41 | if os_is_windows(): 42 | # we do not support for now shrink on windows 43 | Console.error("detect: Windws is not yet supported") 44 | sys.exit() 45 | if image is None: 46 | Console.error("Image must have a value") 47 | image = path_expand(image) 48 | command = f"sudo /usr/local/bin/pishrink.sh {image}" 49 | print(command) 50 | os.system(command) 51 | 52 | def install(self): 53 | """ 54 | Installs /usr/local/bin/pishrink.sh 55 | Installs parted 56 | :return: 57 | :rtype: 58 | """ 59 | 60 | if os_is_mac(): 61 | Console.error("This command is not supported on MacOS") 62 | return "" 63 | elif os_is_windows(): 64 | Console.error("This command is not supported on MacOS") 65 | return "" 66 | else: 67 | banner("Installing pishrink.sh into /usr/local/bin") 68 | script = \ 69 | """ 70 | wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh 71 | chmod +x pishrink.sh 72 | sudo mv pishrink.sh /usr/local/bin 73 | """ 74 | 75 | result = JobScript.execute(script) 76 | print(Printer.write(result, 77 | order=["name", "command", "status", "stdout", "returncode"])) 78 | 79 | if os_is_linux() or os_is_pi(): 80 | banner("Installing pishrink.sh into /usr/local/bin") 81 | script = \ 82 | """ 83 | sudo apt install parted -y > $HOME/tmp.log 84 | """ 85 | 86 | result = JobScript.execute(script) 87 | print(Printer.write(result, 88 | order=["name", "command", "status", "stdout", "returncode"])) 89 | 90 | def firmware(self, action="check"): 91 | self.burner.firmware(action=action) 92 | 93 | def check(self, device=None): 94 | self.burner.check(device=device) 95 | 96 | def configure_wifi(self, ssid, psk=None, country=None, host=None): 97 | self.burner.configure_wifi(ssid, psk=psk, country=country, host=host) 98 | 99 | def mac(self, hostnames=None): 100 | self.burner.mac(hostnames=hostnames) 101 | 102 | def set_hostname(self, hostname): 103 | self.burner.set_hostname(hostname) 104 | 105 | def set_static_ip(self, ip): 106 | self.burner.set_static_ip(ip) 107 | 108 | def set_cmdline(self, cmdline): 109 | self.burner.set_cmdline(cmdline) 110 | 111 | def set_key(self, key): 112 | self.burner.set_key(key) 113 | 114 | def keyboard(self, country=None): 115 | self.burner.keyboard(country=country) 116 | 117 | def enable_ssh(self): 118 | self.burner.enable_ssh() 119 | 120 | def cluster(self, arguments=None): 121 | self.burner.cluster(arguments=arguments) 122 | 123 | # multi_self.burner.burn_inventory 124 | -------------------------------------------------------------------------------- /cloudmesh/burn/burner/BurnerABC.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from cloudmesh.common.parameter import Parameter 4 | 5 | 6 | class AbstractBurner(ABC): 7 | 8 | # @abstractmethod 9 | def cluster(self, arguments=None): 10 | """ 11 | burns a list of cards from the inventory 12 | 13 | :param arguments: 14 | :type arguments: 15 | :return: 16 | :rtype: 17 | """ 18 | burning = Parameter.expand(arguments.burning) 19 | for host in burning: 20 | arguments.burning = burning 21 | self.burn(arguments) 22 | 23 | @abstractmethod 24 | def burn(self, arguments=None): 25 | """ 26 | burns a single card from the inventory 27 | 28 | :param arguments: 29 | :type arguments: 30 | :return: 31 | :rtype: 32 | """ 33 | raise NotImplementedError 34 | 35 | def inventory(self, arguments=None): 36 | """ 37 | Creates the inventory for a cluster 38 | 39 | :param arguments: 40 | :type arguments: 41 | :return: 42 | :rtype: 43 | """ 44 | raise NotImplementedError 45 | -------------------------------------------------------------------------------- /cloudmesh/burn/burner/RaspberryBurner.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | 4 | from cloudmesh.burn.burner.BurnerABC import AbstractBurner 5 | from cloudmesh.burn.raspberryos.cmdline import Cmdline 6 | from cloudmesh.burn.raspberryos.runfirst import Runfirst 7 | from cloudmesh.burn.sdcard import SDCard 8 | from cloudmesh.burn.usb import USB 9 | from cloudmesh.common.console import Console 10 | from cloudmesh.common.parameter import Parameter 11 | from cloudmesh.common.util import yn_choice 12 | from cloudmesh.common.util import readfile 13 | from cloudmesh.common.util import path_expand 14 | from cloudmesh.common.util import banner 15 | from cloudmesh.inventory.inventory import Inventory 16 | from cloudmesh.common.systeminfo import os_is_windows 17 | from cloudmesh.burn.windowssdcard import Diskpart 18 | 19 | 20 | class Burner(AbstractBurner): 21 | """ 22 | Burner uses a cloudmesh inventory to create a RaspberryOS cluster 23 | 24 | Inventory should contain information on manager and workers 25 | """ 26 | 27 | def __init__(self, inventory=None): 28 | # Get inventory 29 | if inventory is None: 30 | inv = Inventory() 31 | else: 32 | inv = Inventory(filename=inventory) 33 | 34 | # Find managers and workers 35 | managers = inv.find(service='manager') 36 | workers = inv.find(service='worker') 37 | 38 | # No inherent need to distinguish the configs by service 39 | configs = managers + workers 40 | # Create dict for them for easy lookup 41 | self.configs = dict((config['host'], config) for config in configs) 42 | 43 | def cluster(self, arguments=None): 44 | raise NotImplementedError 45 | 46 | def burn(self, 47 | name=None, 48 | device=None, 49 | verbose=False, 50 | password=None, 51 | ssid=None, 52 | wifipasswd=None, 53 | country=None, 54 | withimage=True, 55 | network="internal"): 56 | """ 57 | Given the name of a config, burn device with RaspberryOS and configure properly 58 | """ 59 | if device is None: 60 | Console.error('Device not specified') 61 | return 62 | if name is None: 63 | Console.error('Name to burn is not specified') 64 | return 65 | if name not in self.configs: 66 | Console.error(f'Could not find {name} in Inventory. Is the service column marked as "manager" or "worker"?') 67 | return 68 | if country is None: 69 | country = "US" 70 | 71 | config = self.configs[name] 72 | sdcard = SDCard(card_os="raspberry") 73 | 74 | # This block only works for Macs 75 | try: 76 | r = USB.check_for_readers() 77 | except Exception as e: 78 | print() 79 | Console.error(e) 80 | print() 81 | return "" 82 | 83 | banner(txt=f"Create RUNFIRST {name}", figlet=True) 84 | 85 | # Build the proper runfrist.sh 86 | runfirst = Runfirst() 87 | runfirst.set_hostname(config['host']) 88 | other_hosts, other_ips = self._get_hosts_for(name=config['host']) 89 | if network in ['internal']: 90 | runfirst.set_hosts(names=other_hosts, ips=other_ips) 91 | if config['ip']: 92 | # config['router'] and config['dns'] are allowed to be empty String or None to skip its config 93 | # Default column in inventory is empty string 94 | runfirst.set_static_ip(ip=config['ip'], router=config['router'], dns=config['dns']) 95 | 96 | if password: 97 | runfirst.set_password(password=password) 98 | 99 | runfirst.set_locale(timezone=config['timezone'], locale=config['locale']) 100 | if ssid: 101 | runfirst.set_wifi(ssid, wifipasswd, country=country) 102 | 103 | runfirst.set_key(key=readfile(config['keyfile']).strip()) 104 | if 'bridge' in config['services'] and network in ['internal']: 105 | runfirst.enable_bridge() 106 | 107 | runfirst.get(verbose=verbose) 108 | 109 | runfirst.info() 110 | 111 | print(runfirst.script) 112 | 113 | banner(txt=f"Burn {name}", figlet=True) 114 | 115 | card = SDCard().info(print_os=False, print_stdout=False) 116 | if device not in card: 117 | label = "device" 118 | 119 | if os_is_windows(): 120 | label = "disk" 121 | 122 | SDCard().info(print_stdout=True) 123 | 124 | error = f"The {label} {device} could not be found in the list of possible SD Card readers" 125 | Console.error(error) 126 | 127 | if not yn_choice("Would you like to continue?"): 128 | raise ValueError(error) 129 | 130 | # Confirm card is inserted into device path 131 | if not yn_choice(f'Is the card to be burned for {name} inserted?'): 132 | if not yn_choice(f"Please insert the card to be burned for {name}. " 133 | "Type 'y' when done or 'n' to terminate. Continue"): 134 | Console.error("Terminating: User Break") 135 | return "" 136 | 137 | Console.info(f'Burning {name}') 138 | 139 | if os_is_windows(): 140 | if withimage: 141 | sdcard.format_device(device=device, unmount=True) 142 | banner("Burn image", color="GREEN") 143 | sdcard.burn_sdcard(name=name, tag=config['tag'], device=device, yes=True) 144 | 145 | detail = Diskpart.detail(disk=device) 146 | letter = detail["Ltr"] 147 | 148 | # sdcard instance needs the drive letter in order to use 149 | # sdcard.boot_volume later (windows) 150 | sdcard.set_drive(drive=letter) 151 | 152 | # print(f"Letter {letter}") 153 | # yn_choice("Burn completed. Continue") 154 | 155 | else: 156 | if withimage: 157 | sdcard.format_device(device=device, yes=True) 158 | sdcard.unmount(device=device) 159 | banner("Burn image", color="GREEN") 160 | sdcard.burn_sdcard(name=name, tag=config['tag'], device=device, yes=True) 161 | sdcard.mount(device=device, card_os="raspberry") 162 | 163 | # Read and write cmdline.txt 164 | cmdline = Cmdline() 165 | # Reading will create the proper script in the cmdline instance 166 | # No extra work needed 167 | # This gets rid of whitespace in cmdline.txt file? 168 | cmdline.update(filename=f'{sdcard.boot_volume}/cmdline.txt') 169 | cmdline.write(filename='tmp-cmdline.txt') 170 | # print("--- cmdline.txt ---") 171 | # print(cmdline.script) 172 | # print("---") 173 | 174 | # 175 | # write the run first 176 | # 177 | runfirst.write(filename=f'{sdcard.boot_volume}/{Runfirst.SCRIPT_NAME}') 178 | os.system(f"chmod a+x {sdcard.boot_volume}/{Runfirst.SCRIPT_NAME}") 179 | 180 | if not os_is_windows(): 181 | time.sleep(1) # Sleep for 1 seconds to give ample time for writing to finish 182 | sdcard.unmount(device=device, card_os="raspberry") 183 | else: 184 | time.sleep(2) # Sleep for 1 seconds to give ample time for writing to finish 185 | os.system(f"cat {sdcard.boot_volume}/{Runfirst.SCRIPT_NAME}") 186 | Console.ok(f'Burned {name}') 187 | 188 | return 189 | 190 | def inventory(self, arguments=None): 191 | raise NotImplementedError 192 | 193 | def multi_burn(self, 194 | names=None, 195 | devices=None, 196 | verbose=False, 197 | password=None, 198 | ssid=None, 199 | wifipasswd=None, 200 | country=None, 201 | withimage=True, 202 | network="internal" 203 | ): 204 | """ 205 | Given multiple names, burn them 206 | """ 207 | if devices is None: 208 | Console.error('Device not specified.') 209 | return 210 | if names is None: 211 | Console.error('Names to burn not specified') 212 | return 213 | 214 | names = Parameter.expand(names) 215 | devices = Parameter.expand(devices) 216 | 217 | if len(devices) > 1: 218 | Console.error('We do not yet support burning on multiple devices') 219 | return 220 | 221 | for name in names: 222 | self.burn( 223 | name=name, 224 | device=devices[0], 225 | verbose=verbose, 226 | password=password, 227 | ssid=ssid, 228 | wifipasswd=wifipasswd, 229 | country=country, 230 | withimage=withimage, 231 | network=network 232 | ) 233 | Console.ok('Finished burning all cards') 234 | 235 | def _get_hosts_for(self, name=None): 236 | """ 237 | Given a name, return the list of hostnames and ips to go into /etc/hosts 238 | """ 239 | if name is None: 240 | raise Exception('name is None') 241 | if self.configs is None: 242 | raise Exception('no configs supplied yet') 243 | 244 | host_names = [] 245 | ips = [] 246 | 247 | for host_name in self.configs: 248 | if name != host_name and self.configs[host_name]['ip']: 249 | host_names += [host_name] 250 | ips += [self.configs[host_name]['ip']] 251 | return host_names, ips 252 | -------------------------------------------------------------------------------- /cloudmesh/burn/burner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/burner/__init__.py -------------------------------------------------------------------------------- /cloudmesh/burn/burner/ubuntu.py: -------------------------------------------------------------------------------- 1 | # class Burner(AbstractBurner): 2 | class Burner: 3 | 4 | def __init__(self): 5 | pass 6 | -------------------------------------------------------------------------------- /cloudmesh/burn/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/command/__init__.py -------------------------------------------------------------------------------- /cloudmesh/burn/hardware.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import platform 4 | 5 | 6 | class Hardware(object): 7 | 8 | @staticmethod 9 | def is_pi(): 10 | """ 11 | Checks if its called on a PI 12 | 13 | :return: returns true if this is called on a Pi 14 | :rtype: bool 15 | """ 16 | # return os.uname()[4][:3] == 'arm' and 'Raspberry' in Hardware.model() 17 | return platform.uname()[4][:3] == 'arm' and 'Raspberry' in Hardware.model() 18 | 19 | @staticmethod 20 | def get_mac(interface='eth0'): 21 | """ 22 | Get the mac address 23 | 24 | :param interface: the network interface name 25 | :type interface: str 26 | :return: mac address 27 | :rtype: str 28 | """ 29 | # noinspection PyBroadException 30 | try: 31 | address = open(f'/sys/class/net/{interface}/address').read() 32 | except Exception as e: # noqa: F841 33 | address = "00:00:00:00:00:00" 34 | return address[0:17] 35 | 36 | @staticmethod 37 | def get_ethernet(): 38 | """ 39 | TODO: describe 40 | 41 | :return: 42 | :rtype: 43 | """ 44 | interface = None 45 | # noinspection PyBroadException 46 | try: 47 | for root, dirs, files in os.walk('/sys/class/net'): 48 | for directory in dirs: 49 | if directory[:3] == 'enx' or directory[:3] == 'eth': 50 | interface = directory 51 | except Exception as e: # noqa: F841 52 | interface = "None" 53 | return interface 54 | 55 | @staticmethod 56 | def model(): 57 | """ 58 | TODO: describe 59 | 60 | :return: 61 | :rtype: 62 | """ 63 | # noinspection PyBroadException 64 | try: 65 | model_str = open('/sys/firmware/devicetree/base/model').read() 66 | except Exception as e: # noqa: F841 67 | model_str = "unkown" 68 | 69 | return model_str 70 | 71 | @staticmethod 72 | def hostname(): 73 | """ 74 | The hostname 75 | 76 | :return: the hostname 77 | :rtype: str 78 | """ 79 | return socket.gethostname() 80 | 81 | @staticmethod 82 | def fqdn(): 83 | """ 84 | TODO: describe 85 | 86 | :return: 87 | :rtype: 88 | """ 89 | return socket.getfqdn() 90 | 91 | 92 | """ 93 | eth0 = Hardware.get_mac('eth0') 94 | wan = Hardware.get_mac('wlan0') 95 | 96 | print(eth0) 97 | 98 | print(wan) 99 | 100 | print(os.uname()[4][:3] == 'arm') 101 | 102 | print(Hardware.model()) 103 | print(Hardware.is_pi()) 104 | print(Hardware.hostname()) 105 | print(Hardware.fqdn()) 106 | """ 107 | -------------------------------------------------------------------------------- /cloudmesh/burn/images/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/images/__init__.py -------------------------------------------------------------------------------- /cloudmesh/burn/images/cm-logo-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/images/cm-logo-100.png -------------------------------------------------------------------------------- /cloudmesh/burn/images/cm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/images/cm-logo.png -------------------------------------------------------------------------------- /cloudmesh/burn/images/raspberry-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/images/raspberry-logo-black.png -------------------------------------------------------------------------------- /cloudmesh/burn/images/raspberry-logo-white-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/images/raspberry-logo-white-100.png -------------------------------------------------------------------------------- /cloudmesh/burn/images/raspberry-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/images/raspberry-logo-white.png -------------------------------------------------------------------------------- /cloudmesh/burn/network.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | import subprocess 4 | 5 | 6 | # TODO: there seems to be some overlap with hardware.py and some functions in 7 | # hardware introduced that are not used here such as getfqdn and hostname. 8 | # Should it be removed form hardware, or should this be changed here to 9 | # use the methods from hardware? 10 | # 11 | # TODO: get method is not implemented 12 | 13 | class Network: 14 | 15 | # noinspection PyBroadException,PyBroadException 16 | @staticmethod 17 | def address(): 18 | hostname = socket.gethostname() 19 | full = socket.getfqdn() 20 | 21 | # ipv4 22 | result = [] 23 | try: 24 | details = json.loads(subprocess.getoutput("ip -json a")) 25 | except Exception as e: # noqa: F841 26 | details = None 27 | for entry in details: 28 | try: 29 | addresses = entry['addr_info'] 30 | for n in addresses: 31 | if entry['ifname'] not in ['lo']: 32 | element = { 33 | 'ip': socket.gethostbyname(hostname), 34 | 'hostname': hostname, 35 | 'ifname': entry['ifname'], 36 | 'fullname': full, 37 | 'ipbyname': socket.gethostbyname(hostname), 38 | 39 | } 40 | # print ("nnn", n) 41 | element.update(n) 42 | if 'broadcast' in n.keys(): 43 | result.append(element) 44 | except Exception as e: # noqa: F841 45 | pass 46 | return result 47 | 48 | @staticmethod 49 | def nmap(ip=None): 50 | if ip is None: 51 | ip = Network.address()['ip'] 52 | mask = ip.rsplit(".", 1)[0] 53 | command = f"nmap -sP {mask}.*" 54 | print(command) 55 | result = subprocess.getoutput(command) 56 | result = result.replace("Nmap scan report for ", "") 57 | result = result.replace("Host is ", "") 58 | result = result.replace(" latency.", "") 59 | 60 | result = result.splitlines()[1:] 61 | details = [] 62 | for i in range(0, int(len(result) / 2)): 63 | position = 2 * i 64 | a = result[position] 65 | b = result[position + 1] 66 | line = f"{a} {b}" 67 | if "(" not in a: 68 | line = f"unkown {a} {b}" 69 | 70 | line = line.replace("(", "") 71 | line = line.replace(")", "") 72 | 73 | attributes = line.split() 74 | details.append({ 75 | 'name': attributes[0], 76 | 'ip': attributes[1], 77 | 'status': attributes[2], 78 | 'latency': attributes[3], 79 | 'line': line 80 | }) 81 | 82 | return details 83 | -------------------------------------------------------------------------------- /cloudmesh/burn/raspberryos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/raspberryos/__init__.py -------------------------------------------------------------------------------- /cloudmesh/burn/raspberryos/cmdline.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import io 3 | import os 4 | 5 | from cloudmesh.common.Shell import Shell 6 | from cloudmesh.common.util import readfile 7 | from cloudmesh.common.systeminfo import os_is_windows 8 | from cloudmesh.common.util import path_expand 9 | 10 | 11 | class Cmdline: 12 | 13 | def __init__(self): 14 | 15 | self.template = \ 16 | { 17 | "lite": "console=serial0,115200 " + 18 | "console=tty1 " + 19 | "root=PARTUUID={partuuid} " + 20 | "rootfstype=ext4 " + 21 | "elevator=deadline " + 22 | "fsck.repair=yes " + 23 | "rootwait " + 24 | "quiet " + 25 | "init=/usr/lib/raspi-config/init_resize.sh " + 26 | "systemd.run=/boot/firstrun.sh " + 27 | "systemd.run_success_action=reboot " + 28 | "systemd.unit=kernel-command-line.target", 29 | "full": "console=serial0,115200 " + 30 | "console=tty1 " + 31 | "root=PARTUUID={partuuid} " + 32 | "rootfstype=ext4 " + 33 | "elevator=deadline " + 34 | "fsck.repair=yes " + 35 | "rootwait " + 36 | "quiet " + 37 | "init=/usr/lib/raspi-config/init_resize.sh " + 38 | "splash " + 39 | "plymouth.ignore-serial-consoles " + 40 | "systemd.run=/boot/firstrun.sh " + 41 | "systemd.run_success_action=reboot " + 42 | "systemd.unit=kernel-command-line.target" 43 | } 44 | self.template["lite-32"] = self.template["lite"] 45 | self.template["full-32"] = self.template["full"] 46 | self.template["lite-64"] = self.template["lite"] 47 | self.template["full-64"] = self.template["full"] 48 | 49 | # Commented out above since we should just append 50 | # the lines below to the existing cmdline.txt since 51 | # root PARTUUID may vary 52 | 53 | # self.cmdline will be populated when .read() is called 54 | self.cmdline = None 55 | # the space-separated values to add to the end of cmdline 56 | # self.script = " ".join(textwrap.dedent(""" 57 | # splash 58 | # plymouth.ignore-serial-consoles 59 | # systemd.run=/boot/firstrun.sh 60 | # systemd.run_success_action=reboot 61 | # systemd.unit=kernel-command-line.target 62 | # """).splitlines()).strip() 63 | self.script = None 64 | 65 | def update(self, filename, version="lite"): 66 | """ 67 | NEW: 68 | * [ ] TODO: test on windows 69 | * [ ] TODO: test on Linux 70 | * [ ] TODO: test on macOS 71 | 72 | filename: the filename to be changed on the sdkard reade. 73 | On windows you need the driveletter + "cmdline.txt" 74 | """ 75 | self.cmdline = readfile(filename).split(" ") 76 | 77 | for partuuid in self.cmdline: 78 | if partuuid.startswith("root=PARTUUID="): 79 | partuuid = partuuid.split("root=PARTUUID=")[1].strip() 80 | break 81 | self.script = self.template[version].format(partuuid=partuuid) 82 | 83 | self.writefile(filename, self.script) 84 | 85 | def writefile(self, filename, content): 86 | """ 87 | NEW: 88 | * [ ] TODO: test on windows 89 | * [ ] TODO: test on Linux 90 | * [ ] TODO: test on macOS 91 | 92 | writes the content into the file 93 | :param filename: the filename to be changed on the sdcard cmdline.txt. 94 | On Windows it must start with the drive letter such as "f:/" 95 | :param content: the content 96 | :return: 97 | """ 98 | outfile = io.open(path_expand(filename), 'w', newline='\n') 99 | outfile.write(content) 100 | outfile.flush() 101 | os.fsync(outfile) 102 | 103 | def read(self, filename=None): 104 | """ 105 | Read a pre-existing cmdline.txt and store it 106 | """ 107 | if filename is None: 108 | raise Exception("read called with no filename") 109 | self.cmdline = readfile(filename).strip() 110 | 111 | def write(self, filename=None): 112 | """ 113 | Write the cmdline config to the specified filename 114 | """ 115 | if self.cmdline is None: 116 | # Cmdline varies by burn 117 | raise Exception("Please read a pre-existing cmdline.txt first") 118 | if filename is None: 119 | raise Exception("write called with no filename") 120 | if os_is_windows(): 121 | Shell.run(f'echo "{self.cmdline} {self.script}" | tee {filename}') 122 | else: 123 | Shell.run(f'echo "{self.cmdline} {self.script}" | sudo tee {filename}') 124 | 125 | def get(self): 126 | """ 127 | Return the proper cmdline with the necessary commands 128 | """ 129 | if self.cmdline is None: 130 | print("Using example cmdline.txt. Not safe for usage. For testing only") 131 | self.cmdline = self._example() 132 | 133 | return self.cmdline + self.script 134 | 135 | def _example(self): 136 | """ 137 | An example cmdline.txt for testing purposes 138 | """ 139 | return " ".join(textwrap.dedent(""" 140 | console=serial0,115200 141 | console=tty1 142 | root=PARTUUID=9730496b-02 143 | rootfstype=ext4 144 | elevator=deadline 145 | fsck.repair=yes 146 | rootwait 147 | quiet 148 | init=/usr/lib/raspi-config/init_resize.sh 149 | """).splitlines()).strip() 150 | 151 | # root=PARTUUID=904a3764-02 152 | -------------------------------------------------------------------------------- /cloudmesh/burn/raspberryos/passwd.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | 4 | class Passwd: 5 | file = textwrap.dedent(""" 6 | root:x:0:0:root:/root:/bin/bash 7 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 8 | bin:x:2:2:bin:/bin:/usr/sbin/nologin 9 | sys:x:3:3:sys:/dev:/usr/sbin/nologin 10 | sync:x:4:65534:sync:/bin:/bin/sync 11 | games:x:5:60:games:/usr/games:/usr/sbin/nologin 12 | man:x:6:12:man:/var/cache/man:/usr/sbin/nologin 13 | lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin 14 | mail:x:8:8:mail:/var/mail:/usr/sbin/nologin 15 | news:x:9:9:news:/var/spool/news:/usr/sbin/nologin 16 | uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin 17 | proxy:x:13:13:proxy:/bin:/usr/sbin/nologin 18 | www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin 19 | backup:x:34:34:backup:/var/backups:/usr/sbin/nologin 20 | list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin 21 | irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin 22 | gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin 23 | nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin 24 | systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin 25 | systemd-network:x:101:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin 26 | systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin 27 | _apt:x:103:65534::/nonexistent:/usr/sbin/nologin 28 | pi:x:1000:1000:,,,:/home/pi:/bin/bash 29 | messagebus:x:104:110::/nonexistent:/usr/sbin/nologin 30 | _rpc:x:105:65534::/run/rpcbind:/usr/sbin/nologin 31 | statd:x:106:65534::/var/lib/nfs:/usr/sbin/nologin 32 | sshd:x:107:65534::/run/sshd:/usr/sbin/nologin 33 | avahi:x:108:113:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin 34 | lightdm:x:109:114:Light Display Manager:/var/lib/lightdm:/bin/false 35 | rtkit:x:110:116:RealtimeKit,,,:/proc:/usr/sbin/nologin 36 | pulse:x:111:119:PulseAudio daemon,,,:/var/run/pulse:/usr/sbin/nologin 37 | saned:x:112:122::/var/lib/saned:/usr/sbin/nologin 38 | hplip:x:113:7:HPLIP system user,,,:/var/run/hplip:/bin/false 39 | colord:x:114:123:colord colour management daemon,,,:/var/lib/colord:/usr/sbin/nologin 40 | """) 41 | 42 | shadow = textwrap.dedent(""" 43 | root:*:18638:0:99999:7::: 44 | daemon:*:18638:0:99999:7::: 45 | bin:*:18638:0:99999:7::: 46 | sys:*:18638:0:99999:7::: 47 | sync:*:18638:0:99999:7::: 48 | games:*:18638:0:99999:7::: 49 | man:*:18638:0:99999:7::: 50 | lp:*:18638:0:99999:7::: 51 | mail:*:18638:0:99999:7::: 52 | news:*:18638:0:99999:7::: 53 | uucp:*:18638:0:99999:7::: 54 | proxy:*:18638:0:99999:7::: 55 | www-data:*:18638:0:99999:7::: 56 | backup:*:18638:0:99999:7::: 57 | list:*:18638:0:99999:7::: 58 | irc:*:18638:0:99999:7::: 59 | gnats:*:18638:0:99999:7::: 60 | nobody:*:18638:0:99999:7::: 61 | systemd-timesync:*:18638:0:99999:7::: 62 | systemd-network:*:18638:0:99999:7::: 63 | systemd-resolve:*:18638:0:99999:7::: 64 | _apt:*:18638:0:99999:7::: 65 | pi:$6$ZTD6hJH7f.ZyvE3M$Tgfv.ULQpXRtW7YRxImuFX1qrfVO5BGwTm17w/0.WYBiEck6mE5vRcFfJy.NgrYIJqU.aOGpu//hhYwL1uh8T.:18638:0:99999:7::: 66 | messagebus:*:18638:0:99999:7::: 67 | _rpc:*:18638:0:99999:7::: 68 | statd:*:18638:0:99999:7::: 69 | sshd:*:18638:0:99999:7::: 70 | avahi:*:18638:0:99999:7::: 71 | lightdm:*:18638:0:99999:7::: 72 | rtkit:*:18638:0:99999:7::: 73 | pulse:*:18638:0:99999:7::: 74 | saned:*:18638:0:99999:7::: 75 | hplip:*:18638:0:99999:7::: 76 | colord:*:18638:0:99999:7::: 77 | """) 78 | -------------------------------------------------------------------------------- /cloudmesh/burn/raspberryos/test.py: -------------------------------------------------------------------------------- 1 | # 2 | # python cloudmesh/burn/raspberryos/test.py 3 | # 4 | from cloudmesh.burn.raspberryos.cmdline import Cmdline 5 | from cloudmesh.burn.raspberryos.runfirst import Runfirst 6 | from cloudmesh.common.parameter import Parameter 7 | 8 | cmsline = Cmdline() 9 | # cmsline.read(filename='/Volumes/boot/cmdline.txt') 10 | # cmsline.write(filename="test.txt") 11 | 12 | print(cmsline.get()) 13 | print() 14 | 15 | names = Parameter.expand("red,red[0-1]") 16 | ips = Parameter.expand("10.0.0.[1-3]") 17 | 18 | runfirst = Runfirst() 19 | runfirst.set_key() 20 | runfirst.set_wifi("SSID", "PASSWORD") 21 | runfirst.set_locale() 22 | runfirst.set_hostname("red2") 23 | runfirst.set_hosts(names, ips) 24 | runfirst.set_static_ip(ip="10.0.0.4") 25 | runfirst.set_password(password="password") 26 | runfirst.info() 27 | 28 | print(runfirst.get()) 29 | -------------------------------------------------------------------------------- /cloudmesh/burn/ubuntu/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/ubuntu/__init__.py -------------------------------------------------------------------------------- /cloudmesh/burn/ubuntu/configure.py: -------------------------------------------------------------------------------- 1 | from cloudmesh.burn.ubuntu.userdata import Userdata 2 | from cloudmesh.burn.ubuntu.networkdata import Networkdata 3 | from cloudmesh.common.console import Console 4 | from cloudmesh.common.util import readfile 5 | from cloudmesh.inventory.inventory import Inventory 6 | from cloudmesh.common.Shell import Shell 7 | from cloudmesh.common.util import path_expand 8 | 9 | 10 | class Configure: 11 | """ 12 | This class serves to build cloud-init config files for entries in a cloudmesh inventory file. 13 | This accepts two params for init: 14 | inventory = INVENTORY_FILE 15 | cluster = CLUSTER_NAME 16 | 17 | If inventory arg is None, then default ~/.cloudmesh/inventory.yaml is used 18 | 19 | if cluster arg is not None, then nodes are found by searching via the 20 | "cluster" column in inventory 21 | 22 | if cluster arg is None, then nodes are found by searching for "worker" and "manager" in the 23 | "service" column in inventory 24 | 25 | Usage: 26 | config_generator = Configure() 27 | Configure.build_user_data(name=NAME) returns a Userdata builder object 28 | where NAME is the hostname of an entry in inventory.yaml with corresponding config options 29 | """ 30 | KEY_DIR = path_expand('~/.cloudmesh/cmburn') 31 | 32 | def __init__(self, inventory=None, cluster=None, debug=False): 33 | self.debug = debug 34 | self.manager_public_key = None # Populated by self.generate_ssh_key 35 | 36 | if inventory: 37 | self.inventory = Inventory(inventory) 38 | else: 39 | self.inventory = Inventory() 40 | 41 | if cluster is not None: 42 | self.nodes = self.inventory.find(cluster=cluster) 43 | else: 44 | self.nodes = self.inventory.find(service='manager') + self.inventory.find(service='worker') 45 | 46 | self.manager_public_key = None 47 | 48 | def build_user_data(self, name=None, with_defaults=True, country=None, 49 | add_manager_key=False, upgrade=False, with_bridge=False): 50 | """ 51 | Given a name, get its config from self.inventory and create a Userdata object 52 | """ 53 | if name is None: 54 | raise Exception('name arg supplied is None') 55 | elif not self.inventory.has_host(name): 56 | raise Exception(f'Could not find {name} in {self.inventory.filename}') 57 | if country is not None and len(country) != 2: 58 | raise Exception('Country code is not 2 characters.') 59 | 60 | # Get the current configurations from inventory 61 | hostname = self.inventory.get(name=name, attribute='host') 62 | keyfile = self.inventory.get(name=name, attribute='keyfile') 63 | if keyfile: 64 | keys = readfile(keyfile).strip().split('\n') 65 | else: 66 | keys = None 67 | 68 | if keys is None and add_manager_key: 69 | keys = [self.manager_public_key] 70 | elif add_manager_key: 71 | keys.append(self.manager_public_key) 72 | 73 | service = self.inventory.get(name=name, attribute='service') 74 | 75 | # Build Userdata 76 | user_data = Userdata() 77 | 78 | if with_defaults: 79 | user_data.with_locale().with_net_tools().with_packages( 80 | packages='avahi-daemon') 81 | if upgrade: 82 | user_data.with_package_update().with_package_upgrade() 83 | if hostname: 84 | user_data.with_hostname(hostname=hostname) 85 | if keys: 86 | # Disable password auth in favor of key auth 87 | user_data.with_ssh_password_login(ssh_pwauth=False) 88 | user_data.with_authorized_keys(keys=keys) 89 | else: 90 | user_data.with_default_user().with_ssh_password_login() 91 | # Add known hosts 92 | user_data.with_hosts(hosts=self.get_hosts_for(name=name)) 93 | if country: 94 | user_data.with_set_wifi_country(country=country) 95 | if service == 'manager' and self.manager_public_key: 96 | # If public key is set, then we expect /boot/firmware/id_rsa and /boot/firmware/id_rsa.pub on burned card 97 | user_data.with_runcmd(cmd='cat /boot/firmware/id_rsa.pub > /home/ubuntu/.ssh/id_rsa.pub')\ 98 | .with_runcmd(cmd='cat /boot/firmware/id_rsa > /home/ubuntu/.ssh/id_rsa')\ 99 | .with_fix_user_dir_owner(user='ubuntu')\ 100 | .with_runcmd(cmd='chmod 600 /home/ubuntu/.ssh/id_rsa')\ 101 | .with_runcmd(cmd='sudo rm /boot/firmware/id_rsa.pub')\ 102 | .with_runcmd(cmd='sudo rm /boot/firmware/id_rsa') 103 | if with_bridge: 104 | user_data.with_access_point_bridge() 105 | 106 | if self.debug: 107 | Console.info(f'User data for {name}:\n' + str(user_data)) 108 | 109 | return user_data 110 | 111 | def build_network_data(self, name=None, ssid=None, password=None, 112 | with_defaults=True): 113 | """ 114 | Given a name, get its config from self.inventory and create a Networkdata object 115 | """ 116 | if name is None: 117 | raise Exception('name arg supplied is None') 118 | elif not self.inventory.has_host(name): 119 | raise Exception(f'Could not find {name} in {self.inventory.filename}') 120 | if ssid or password: 121 | if not ssid or not password: 122 | raise Exception("ssid or password supplied with no corresponding ssid or password") 123 | 124 | # Get the current configurations from inventory 125 | eth0_ip = self.inventory.get(name=name, attribute='ip') 126 | eth0_nameservers = self.inventory.get(name=name, attribute='dns') 127 | eth0_gateway = self.inventory.get(name=name, attribute='router') 128 | 129 | network_data = Networkdata() 130 | 131 | if with_defaults: 132 | network_data.with_defaults() 133 | if eth0_ip: 134 | network_data.with_ip(ip=eth0_ip) 135 | if eth0_nameservers: 136 | network_data.with_nameservers(nameservers=eth0_nameservers) 137 | if eth0_gateway: 138 | network_data.with_gateway(gateway=eth0_gateway) 139 | if ssid and password: 140 | Console.info(f'Providing WiFi access to {name}') 141 | network_data.with_access_points(ssid=ssid, password=password)\ 142 | .with_dhcp4(interfaces='wifis', interface='wlan0', dhcp4=True)\ 143 | .with_optional(interfaces='wifis', interface='wlan0', optional=True) 144 | 145 | if self.debug: 146 | Console.info(f'Network data for {name}:\n' + str(network_data)) 147 | 148 | return network_data 149 | 150 | def get_hosts_for(self, name=None): 151 | """ 152 | Given a hostname, return a list of ':' separated strings of the form: 153 | 154 | ip:hostname 155 | 156 | for all hosts with ips in the inventory. Also includes mapping for own hostname in 157 | the form of 127.0.0.1:{name} 158 | 159 | For example, if inventory has 160 | worker001 with ip 10.1.1.1, 161 | worker002 with ip 10.1.1.2, 162 | worker003 with ip 10.1.1.3, 163 | then: 164 | 165 | self.get_hosts_for(name='worker001') returns 166 | ['127.0.0.1:worker001', 167 | '10.1.1.2:worker002', 168 | '10.1.1.3:worker003'] 169 | 170 | Do not rely on the order of the result here 171 | """ 172 | if name is None: 173 | raise Exception('name arg supplied is None') 174 | if not self.inventory.has_host(name): 175 | raise Exception(f'{name} could not be found in {self.inventory.filename}') 176 | 177 | result = [f'127.0.0.1:{name}'] 178 | for node in self.nodes: 179 | if node['ip'] and name != node['host']: 180 | host = node['host'] 181 | ip = node['ip'] 182 | result += [f'{ip}:{host}'] 183 | return result 184 | 185 | def generate_ssh_key(self, hostname): 186 | Shell.execute('mkdir', f'-p {Configure.KEY_DIR}') 187 | Shell.run(f'ssh-keygen -q -N "" -C "ubuntu@{hostname}" -f ' f'{Configure.KEY_DIR}/id_rsa') 188 | priv_key = readfile(f'{Configure.KEY_DIR}/id_rsa').strip() 189 | pub_key = readfile(f'{Configure.KEY_DIR}/id_rsa.pub').strip() 190 | self.manager_public_key = pub_key 191 | Shell.execute('rm', path_expand(f'{Configure.KEY_DIR}/id_rsa')) 192 | Shell.execute('rm', path_expand(f'{Configure.KEY_DIR}/id_rsa.pub')) 193 | return priv_key, pub_key 194 | -------------------------------------------------------------------------------- /cloudmesh/burn/ubuntu/networkdata.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | from cloudmesh.common.console import Console 4 | from cloudmesh.common.Shell import Shell 5 | from cloudmesh.common.util import path_expand, writefile 6 | 7 | 8 | class Networkdata: 9 | """ 10 | A builder for content in the network-config file with cloud-init 11 | https://refactoring.guru/design-patterns/builder 12 | 13 | A Builder class follows the Builder design pattern. This design pattern allows 14 | users of the class to construct a complex data structure using purely instance 15 | methods. This is very useful in that the user of the class need not concern 16 | themselves with the underlying implementation of the class. Consequentially, 17 | changes to this class can be made without needing a refactor of all existing calls. 18 | 19 | More details on the builder design pattern found here. 20 | https://refactoring.guru/design-patterns/builder 21 | 22 | Example: 23 | d = Networkdata()\ 24 | .with_ip(ip="10.1.1.10")\ 25 | .with_gateway(gateway="10.1.1.1")\ 26 | .with_nameservers(nameservers=['8.8.8.8', '8.8.4.4'])\ 27 | .with_defaults()\ 28 | .with_dhcp4(interfaces='wifis', interface='wlan0', dhcp4=True)\ 29 | .with_optional(interfaces='wifis', interface='wlan0', optional=True)\ 30 | .with_access_points(interfaces='wifis', interface='wlan0', ssid='MYSSID', 31 | password='MYPASSWORD') 32 | 33 | print(d) 34 | 35 | To write to file: 36 | d = Networkdata()\ 37 | .with_ip(ip='10.1.1.10')\ 38 | .with_gateway(gateway='10.1.1.1')\ 39 | .with_nameservers(nameservers=['8.8.8.8', '8.8.4.4'])\ 40 | .with_defaults()\ 41 | .with_dhcp4(interfaces='wifis', interface='wlan0', dhcp4=True)\ 42 | .with_optional(interfaces='wifis', interface='wlan0', optional=True)\ 43 | .with_access_points(ssid='MYSSID', password='MYPASSWORD')\ 44 | .write(filename='test.tmp') 45 | """ 46 | 47 | def __init__(self, version=2, default=False): 48 | # Dict will be dumped into YAML string 49 | self.content = {"version": 2, "ethernets": {}, "wifis": {}} 50 | if default: 51 | self.__default__() 52 | 53 | def __str__(self): 54 | return yaml.dump(self.content) 55 | 56 | def write(self, filename=None): 57 | """ 58 | Writes a file to a location. Safe write for files on mounted partitions 59 | """ 60 | if filename is None: 61 | raise Exception('filename arg supplied is None') 62 | tmp_location = path_expand('~/.cloudmesh/network-data.tmp') 63 | writefile(tmp_location, str(self)) 64 | Console.info(f'Writing to {filename}') 65 | Shell.run(f'cat {tmp_location} | sudo tee {filename}') 66 | 67 | def with_ip(self, interfaces='ethernets', interface='eth0', ip=None): 68 | if ip is None: 69 | raise Exception("ip argument supplied is None") 70 | 71 | # If subnet not specified, default to 255.255.255.0 72 | if "/" not in ip: 73 | ip += "/24" 74 | 75 | if interface not in self.content[interfaces]: 76 | self.content[interfaces][interface] = {} 77 | 78 | self.content[interfaces][interface]['addresses'] = [ip] # Expects a list value 79 | self.content[interfaces][interface]['dhcp4'] = 'no' 80 | return self 81 | 82 | def with_gateway(self, interfaces='ethernets', interface='eth0', gateway=None): 83 | if gateway is None: 84 | raise Exception("gateway argument supplied is None") 85 | 86 | if interface not in self.content[interfaces]: 87 | self.content[interfaces][interface] = {} 88 | 89 | self.content[interfaces][interface]['gateway4'] = gateway 90 | return self 91 | 92 | def with_nameservers(self, interfaces='ethernets', interface='eth0', nameservers=None): 93 | if nameservers is None: 94 | raise Exception("nameservers argument suppliled is None") 95 | if type(nameservers) != list: 96 | raise TypeError("Expected type of nameservers to be a list") 97 | 98 | if interface not in self.content[interfaces]: 99 | self.content[interfaces][interface] = {} 100 | 101 | self.content[interfaces][interface]['nameservers'] = {'addresses': nameservers} 102 | return self 103 | 104 | def with_dhcp4(self, interfaces='ethernets', interface='eth0', dhcp4=True): 105 | if interface not in self.content[interfaces]: 106 | self.content[interfaces][interface] = {} 107 | 108 | self.content[interfaces][interface]['dhcp4'] = dhcp4 109 | 110 | return self 111 | 112 | def with_optional(self, interfaces='ethernets', interface='eth0', optional=True): 113 | if interface not in self.content[interfaces]: 114 | self.content[interfaces][interface] = {} 115 | 116 | self.content[interfaces][interface]['optional'] = optional 117 | 118 | return self 119 | 120 | def with_access_points(self, interfaces='wifis', interface='wlan0', 121 | ssid=None, password=None): 122 | if ssid is None: 123 | raise Exception("ssid argument suppliled is None") 124 | if password is None: 125 | raise Exception("password argument suppliled is None") 126 | 127 | if interface not in self.content[interfaces]: 128 | self.content[interfaces][interface] = {} 129 | 130 | if 'access-points' in self.content[interfaces][interface]: 131 | access_points = self.content[interfaces][interface]['access-points'] 132 | access_points[ssid] = {'password': password} 133 | else: 134 | self.content[interfaces][interface]['access-points'] = { 135 | ssid: {'password': password} 136 | } 137 | 138 | return self 139 | 140 | def with_defaults(self, interfaces='ethernets', interface='eth0'): 141 | """ 142 | Unsure if this is needed, however these params were included in the default config, so we keep 143 | """ 144 | if interface not in self.content[interfaces]: 145 | self.content[interfaces][interface] = {} 146 | 147 | self.content[interfaces][interface]['match'] = {"driver": "bcmgenet smsc95xx lan78xx"} 148 | self.content[interfaces][interface]['set-name'] = interface 149 | return self 150 | 151 | def __default__(self): 152 | """ 153 | Set the default configuration the one that comes burnt with the ubuntu server OS 154 | 155 | Captured with 156 | $ grep - Fv \\ # /{mountpoint}/network-config 157 | (Removes comments) 158 | """ 159 | self.content['ethernets'] = { 160 | "eth0": { 161 | "match": { 162 | "driver": "bcmgenet smsc95xx lan78xx" 163 | }, 164 | "set-name": "eth0", 165 | "dhcp4": True, 166 | "optional": True 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /cloudmesh/burn/ubuntu/userdata.py: -------------------------------------------------------------------------------- 1 | import oyaml as yaml 2 | 3 | from cloudmesh.common.console import Console 4 | from cloudmesh.common.Shell import Shell 5 | from cloudmesh.common.util import path_expand, writefile 6 | 7 | 8 | class Userdata: 9 | """ 10 | A Builder class for cloud-config Userdata 11 | 12 | A Builder class follows the Builder design pattern. This design pattern allows 13 | users of the class to construct a complex data structure using purely instance 14 | methods. This is very useful in that the user of the class need not concern 15 | themselves with the underlying implementation of the class. Consequentially, 16 | changes to this class can be made without needing a refactor of all existing calls. 17 | 18 | Examples: 19 | 20 | d = Userdata()\ 21 | .with_ssh_password_login()\ 22 | .with_locale()\ 23 | .with_hostname(hostname='testserver')\ 24 | .with_default_user()\ 25 | .with_authorized_keys(key=['ssh-rsa AAAA.....user@laptop'])\ 26 | .with_set_wifi_country(country='US')\ 27 | .with_write_files(path='/home/ubuntu/.ssh/id_rsa', content='this is a ssh key', permissions='0600')\ 28 | .with_write_files(path='/home/ubuntu/.ssh/id_rsa.pub', content='this is a pub key', permissions='0644')\ 29 | .with_fix_user_dir_owner(user='ubuntu') 30 | 31 | print(d) 32 | 33 | #cloud-config 34 | ssh_pwauth: yes 35 | locale: en_US 36 | preserve_hostname: false 37 | hostname: testserver 38 | chpasswd: 39 | expire: true 40 | list: 41 | - ubuntu: ubuntu 42 | ssh_authorized_keys: 43 | - ssh-rsa AAAA.....user@laptop 44 | bootcmd: 45 | - - sudo 46 | - iw 47 | - reg 48 | - set 49 | - US 50 | runcmd: 51 | - - sh 52 | - -xc 53 | - sudo echo REGDOMAIN=US | sudo tee /etc/default/crda > /dev/null 54 | - - sh 55 | - -xc 56 | - sudo chown -R ubuntu:ubuntu /home/ubuntu 57 | write_files: 58 | - content: this is a ssh key 59 | path: /home/ubuntu/.ssh/id_rsa 60 | permissions: '0600' 61 | - content: this is a pub key 62 | path: /home/ubuntu/.ssh/id_rsa.pub 63 | permissions: '0644' 64 | 65 | """ 66 | 67 | HEADER = "#cloud-config" 68 | 69 | def __init__(self, default=False): 70 | self.content = {} 71 | if default: 72 | self.__default__() 73 | 74 | def __str__(self): 75 | return Userdata.HEADER + '\n' + yaml.dump(self.content) 76 | 77 | def with_authorized_keys(self, keys=None): 78 | """ 79 | Adds a list of authorized keys for default user ubuntu 80 | """ 81 | if keys is None: 82 | raise Exception('keys arg supplied is None') 83 | if type(keys) != list: 84 | raise TypeError('Expected type of keys to be a list') 85 | 86 | if 'ssh_authorized_keys' not in self.content: 87 | self.content['ssh_authorized_keys'] = keys 88 | else: 89 | self.content['ssh_authorized_keys'] += keys 90 | return self 91 | 92 | def write(self, filename=None): 93 | """ 94 | Writes a file to a location. Safe write for files on mounted partitions 95 | """ 96 | if filename is None: 97 | raise Exception('filename arg supplied is None') 98 | tmp_location = path_expand('~/.cloudmesh/user-data.tmp') 99 | writefile(tmp_location, str(self)) 100 | Console.info(f'Writing to {filename}') 101 | Shell.run(f'cat {tmp_location} | sudo tee {filename}') 102 | 103 | def with_ssh_password_login(self, ssh_pwauth=True): 104 | if ssh_pwauth is None: 105 | raise Exception('ssh_pwauth arg supplied is None') 106 | self.content['ssh_pwauth'] = ssh_pwauth 107 | return self 108 | 109 | def with_package_update(self, update=True): 110 | self.content['package_update'] = update 111 | return self 112 | 113 | def with_package_upgrade(self, upgrade=True): 114 | self.content['package_upgrade'] = upgrade 115 | return self 116 | 117 | def with_set_wifi_country(self, country=None): 118 | if country is None: 119 | raise Exception('the country arg supplied is none') 120 | 121 | cmd = f"sudo iw reg set {country}" 122 | self.with_bootcmd(cmd) 123 | 124 | cmd = f"sudo echo REGDOMAIN={country} | sudo tee /etc/default/crda > /dev/null" 125 | self.with_runcmd(cmd) 126 | return self 127 | 128 | def with_hosts(self, hosts=None): 129 | """ 130 | Give a list of ip:hostname formatted strings, add to /etc/hosts 131 | """ 132 | if not hosts: 133 | raise Exception('hosts arg passed is None') 134 | 135 | for hostpair in hosts: 136 | if ':' not in hostpair: 137 | raise Exception('Expected hosts to be a list of ip:hostname strings') 138 | ip, hostname = hostpair.split(':') 139 | self.with_bootcmd(cmd=f'echo {ip} {hostname} >> /etc/hosts') 140 | return self 141 | 142 | def with_bootcmd(self, cmd=None): 143 | if cmd is None: 144 | raise Exception('the command arg supplied is none') 145 | 146 | if 'bootcmd' in self.content: 147 | if type(cmd) == str: 148 | self.content['bootcmd'].append(cmd) 149 | elif type(cmd) == list: 150 | self.content['bootcmd'].extend(cmd) 151 | else: 152 | if type(cmd) == str: 153 | self.content['bootcmd'] = [cmd] 154 | elif type(cmd) == list: 155 | self.content['bootcmd'] = cmd 156 | return self 157 | 158 | def with_runcmd(self, cmd=None): 159 | if cmd is None: 160 | raise Exception('the command arg supplied is none') 161 | 162 | if 'runcmd' in self.content: 163 | if type(cmd) == str: 164 | self.content['runcmd'].append(cmd) 165 | elif type(cmd) == list: 166 | self.content['runcmd'].extend(cmd) 167 | else: 168 | if type(cmd) == str: 169 | self.content['runcmd'] = [cmd] 170 | elif type(cmd) == list: 171 | self.content['runcmd'] = cmd 172 | return self 173 | 174 | def with_access_point_bridge(self, priv_interface='eth0', ext_interface='wlan0'): 175 | """ 176 | Uses iptables to configure an access point bridge where devices on priv_interface 177 | can route internet traffice through the priv_interface of the device towards ext_interface 178 | 179 | Often, this is practical for providing an entire cluster internet through a centralized point (the manager) 180 | """ 181 | self.with_packages(packages=["iptables-persistent"]) 182 | # Enable ipv4 forwarding and configure ip tables rules 183 | self.with_runcmd(cmd="sudo sysctl -w net.ipv4.ip_forward=1")\ 184 | .with_runcmd(cmd="sudo sed -i 's/#net\\.ipv4\\.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf")\ 185 | .with_runcmd(cmd=f"sudo iptables -A FORWARD -i {priv_interface} -o {ext_interface} -j ACCEPT")\ 186 | .with_runcmd(cmd=f"sudo iptables -A FORWARD -i {ext_interface} -o {priv_interface} " 187 | "-m state --state ESTABLISHED,RELATED -j ACCEPT")\ 188 | .with_runcmd(cmd=f"sudo iptables -t nat -A POSTROUTING -o {ext_interface} -j MASQUERADE")\ 189 | .with_runcmd(cmd="iptables-save > /etc/iptables/rules.v4") # nqa: E501 190 | 191 | return self 192 | 193 | def with_write_files(self, 194 | encoding=None, 195 | content=None, 196 | owner=None, 197 | path=None, 198 | permissions=None): 199 | arguments = locals() 200 | arguments['self'] = None 201 | arguments = [(k, v) for k, v in arguments.items() if v is not None] 202 | 203 | if path is None: 204 | raise Exception('the path arg supplied is none') 205 | if content is None: 206 | raise Exception('the content supplied is none') 207 | 208 | # supports multiline strings, keeps newlines 209 | if '\n' in content: 210 | content = '|\n' + content 211 | 212 | file = {} 213 | for arg in arguments: 214 | file[arg[0]] = arg[1] 215 | 216 | if 'write_files' in self.content: 217 | self.content['write_files'].append(file) 218 | else: 219 | self.content['write_files'] = [file] 220 | 221 | return self 222 | 223 | def with_fix_user_dir_owner(self, user=None): 224 | if user is None: 225 | raise Exception('the user arg supplie is none') 226 | 227 | cmd = f'sudo chown -R {user}:{user} /home/{user}' 228 | self.with_runcmd(cmd=cmd) 229 | return self 230 | 231 | def with_packages(self, packages=None): 232 | """ 233 | Given a list of packages or single package as string, add to the installation list. Can be called multiple times 234 | """ 235 | if packages is None: 236 | raise Exception('ssh_pwauth arg supplied is None') 237 | if type(packages) != list and type(packages) != str: 238 | raise Exception('Type of packages expected to be a list or str') 239 | if 'packages' not in self.content: 240 | self.content['packages'] = [] 241 | 242 | if type(packages) == list: 243 | self.content['packages'] += packages 244 | elif type(packages) == str: 245 | self.content['packages'] += [packages] 246 | return self 247 | 248 | def with_net_tools(self): 249 | """ 250 | Adds net-tools and inetutils-traceroute to package installation list 251 | 252 | For useful network commands such as route and traceroute 253 | """ 254 | self.with_packages(packages=['net-tools', 'inetutils-traceroute']) 255 | return self 256 | 257 | def with_locale(self, locale='en_US'): 258 | if locale is None: 259 | raise Exception('locale arg supplied is None') 260 | self.content['locale'] = locale 261 | return self 262 | 263 | def with_hostname(self, hostname=None): 264 | if hostname is None: 265 | raise Exception('hostname arg supplied is None') 266 | self.content['preserve_hostname'] = False 267 | self.content['hostname'] = hostname 268 | return self 269 | 270 | def with_default_user(self): 271 | """ 272 | Include default user ubuntu with default password ubuntu. Prompt password change upon first access 273 | """ 274 | if 'chpasswd' not in self.content: 275 | self.content['chpasswd'] = {} 276 | 277 | self.content['chpasswd']['expire'] = True 278 | self.content['chpasswd']['list'] = ['ubuntu:ubuntu'] 279 | return self 280 | 281 | def __default__(self): 282 | """ 283 | Set the default configuration the one that comes burnt with the ubuntu server OS 284 | 285 | Captured with 286 | $ grep -Fv \\# /{mountpoint}/user-data 287 | (Removes comments) 288 | """ 289 | self.with_default_user().with_ssh_password_login() 290 | -------------------------------------------------------------------------------- /cloudmesh/burn/util.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import platform 4 | import sys 5 | 6 | from cloudmesh.common.console import Console 7 | from cloudmesh.common.util import readfile 8 | 9 | # import mmap 10 | 11 | BUF_SIZE = 65536 12 | 13 | 14 | def sha1sum(filename=None): 15 | Console.info("Verifying sha1") 16 | h = hashlib.sha1() 17 | with open(filename, 'rb') as f: 18 | # with mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) as mm: 19 | # h.update(mm) 20 | while True: 21 | data = f.read(BUF_SIZE) 22 | if not data: 23 | break 24 | h.update(data) 25 | return h.hexdigest() 26 | 27 | 28 | def sha256sum(filename=None): 29 | Console.info("Verifying sha256") 30 | h = hashlib.sha256() 31 | with open(filename, 'rb') as f: 32 | # with mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) as mm: 33 | # h.update(mm) 34 | while True: 35 | data = f.read(BUF_SIZE) 36 | if not data: 37 | break 38 | h.update(data) 39 | return h.hexdigest() 40 | -------------------------------------------------------------------------------- /cloudmesh/burn/wifi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudmesh/cloudmesh-pi-burn/3d80a6ef4088893b386a225d3fbdefcb54c2cb55/cloudmesh/burn/wifi/__init__.py -------------------------------------------------------------------------------- /cloudmesh/burn/wifi/network-config: -------------------------------------------------------------------------------- 1 | # This file contains a netplan-compatible configuration which cloud-init will 2 | # apply on first-boot (note: it will *not* update the config after the first 3 | # boot). Please refer to the cloud-init documentation and the netplan reference 4 | # for full details: 5 | # 6 | # https://cloudinit.readthedocs.io/en/latest/topics/network-config.html 7 | # https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html 8 | # https://netplan.io/reference 9 | # 10 | # Please note that the YAML format employed by this file is sensitive to 11 | # differences in whitespace; if you are editing this file in an editor (like 12 | # Notepad) which uses literal tabs, take care to only use spaces for 13 | # indentation. See the following link for more details: 14 | # 15 | # https://en.wikipedia.org/wiki/YAML 16 | # 17 | # Some additional examples are commented out below 18 | 19 | version: 2 20 | ethernets: 21 | eth0: 22 | # Rename the built-in ethernet device to "eth0" 23 | match: 24 | driver: bcmgenet smsc95xx lan78xx 25 | set-name: eth0 26 | dhcp4: true 27 | optional: true 28 | wifis: 29 | wlan0: 30 | dhcp4: true 31 | optional: true 32 | access-points: 33 | {ssid}: 34 | password: "{password}" 35 | # myworkwifi: 36 | # password: "correct battery horse staple" 37 | # workssid: 38 | # auth: 39 | # key-management: eap 40 | # method: peap 41 | # identity: "me@example.com" 42 | # password: "passw0rd" 43 | # ca-certificate: /etc/my_ca.pem 44 | -------------------------------------------------------------------------------- /cloudmesh/burn/wifi/provider.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of a function the set the WIFI configuration. 3 | This function is primarily developed for a Raspberry PI 4 | """ 5 | 6 | from cloudmesh.burn.wifi.raspberryos import Wifi as WifiRaspberryOs 7 | from cloudmesh.burn.wifi.ubuntu import Wifi as WifiUbuntu 8 | 9 | 10 | # noinspection PyPep8Naming 11 | def Wifi(card_os="raspberry"): 12 | if card_os == "raspberry": 13 | return WifiRaspberryOs 14 | else: 15 | return WifiUbuntu 16 | -------------------------------------------------------------------------------- /cloudmesh/burn/wifi/raspberryos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of a function the set the WIFI configuration. 3 | This function is primarily developed for a Raspberry PI 4 | """ 5 | import textwrap 6 | 7 | from cloudmesh.common.console import Console 8 | from cloudmesh.common.util import writefile 9 | from cloudmesh.common.sudo import Sudo 10 | 11 | 12 | class Wifi: 13 | """ 14 | The class is used to group a number of useful variables and functions so 15 | it is easier to program and manage Wifi configurations. 16 | 17 | The default location for the configuration file is 18 | 19 | /etc/wpa_supplicant/wpa_supplicant.conf 20 | 21 | """ 22 | 23 | location = "/etc/wpa_supplicant/wpa_supplicant.conf" 24 | 25 | template_key = textwrap.dedent(""" 26 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 27 | update_config=1 28 | country={country} 29 | 30 | network={{ 31 | ssid="{ssid}" 32 | key_mgmt=NONE 33 | }} 34 | """) 35 | 36 | template_psk = textwrap.dedent(""" 37 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 38 | update_config=1 39 | country=US 40 | 41 | network={{ 42 | ssid="{ssid}" 43 | psk="{password}" 44 | key_mgmt=WPA-PSK 45 | }} 46 | """) # noqa: W293 47 | 48 | template = template_psk 49 | 50 | @staticmethod 51 | def set(ssid=None, 52 | password=None, 53 | country="US", 54 | psk=True, 55 | location=location, 56 | sudo=False): 57 | """ 58 | Sets the wifi. Only works for psk based wifi 59 | 60 | :param ssid: The ssid 61 | :type ssid: str 62 | :param password: The password 63 | :type password: str 64 | :param country: Two digit country code 65 | :type country: str 66 | :param psk: If true uses psk authentication 67 | :type psk: bool 68 | :param location: The file where the configuration file should be written to 69 | :type location: str 70 | :param sudo: If tru the write will be done with sudo 71 | :type sudo: bool 72 | :return: True if success 73 | :rtype: bool 74 | """ 75 | 76 | if ssid is None or (psk and password is None): 77 | Console.error("SSID or password not set") 78 | return False 79 | 80 | if psk: 81 | config = Wifi.template.format(**locals()) 82 | else: 83 | config = Wifi.template_key.format(**locals()) 84 | try: 85 | if sudo: 86 | Sudo.writefile(location, config) 87 | else: 88 | writefile(location, config) 89 | 90 | except FileNotFoundError as e: # noqa: F841 91 | Console.error(f"The file does not exist: {location}") 92 | return False 93 | return True 94 | -------------------------------------------------------------------------------- /cloudmesh/burn/wifi/ssid.py: -------------------------------------------------------------------------------- 1 | from cloudmesh.common.Shell import Shell 2 | from cloudmesh.common.systeminfo import os_is_linux 3 | from cloudmesh.common.systeminfo import os_is_mac 4 | from cloudmesh.common.systeminfo import os_is_pi 5 | from cloudmesh.common.systeminfo import os_is_windows 6 | from cloudmesh.common.Printer import Printer 7 | from cloudmesh.common.util import yn_choice 8 | from cloudmesh.common.util import Console 9 | from cloudmesh.common.prettytable import PrettyTable 10 | import subprocess 11 | 12 | 13 | def get_ssid(): 14 | ssid = "" 15 | try: 16 | if os_is_mac(): 17 | command = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I" 18 | r = Shell.run(command).replace("\t", "").splitlines() 19 | ssid = Shell.cm_grep(r, " SSID:")[0].split(":")[1].strip() 20 | elif os_is_linux(): 21 | command = "iwgetid -r" 22 | ssid = Shell.run(command).strip() 23 | elif os_is_pi(): 24 | command = "iwgetid -r" 25 | ssid = Shell.run(command).strip() 26 | elif os_is_windows(): 27 | try: 28 | try: 29 | r = Shell.run('netsh wlan show interfaces').strip().splitlines() 30 | ssid = Shell.cm_grep(r, ' SSID')[0].split(':')[1].strip() 31 | except subprocess.CalledProcessError as e: 32 | if "The Wireless AutoConfig Service (wlansvc) is not running" in str(e.output): 33 | print("Machine is not configured for wifi") 34 | except: 35 | command = "netsh wlan show profiles" 36 | r = Shell.run(command) # .splitlines() 37 | r = Shell.cm_grep(r, "User Profile") 38 | r = [line.split(":")[1].strip() for line in r] 39 | x = PrettyTable(["SSIDs"]) 40 | for item in r: 41 | x.add_row([item]) 42 | x.align = "l" 43 | x.align["SSID"] = "l" 44 | looping = True 45 | while looping: 46 | print(x) 47 | ssid = input('Enter ssid from list:\n') 48 | if ssid not in r: 49 | if not yn_choice(f'The entered SSID is not in the list. Are you sure you want to ' 50 | f'use {ssid}? (type Y and press Enter to use {ssid}) '): 51 | Console.ok('Showing the list of SSIDs again...\n') 52 | else: 53 | looping = False 54 | else: 55 | looping = False 56 | except: # noqa 57 | pass 58 | return ssid 59 | -------------------------------------------------------------------------------- /cloudmesh/burn/wifi/ubuntu.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of a function the set the WIFI configuration. 3 | This function is primarily developed for a Raspberry PI 4 | """ 5 | import textwrap 6 | 7 | from cloudmesh.common.console import Console 8 | from cloudmesh.common.sudo import Sudo 9 | from cloudmesh.common.util import writefile 10 | 11 | 12 | class Wifi: 13 | """ 14 | The class is used to group a number of useful variables and functions so 15 | it is easier to program and manage Wifi configurations. 16 | 17 | The default location for the configuration file is 18 | 19 | /etc/wpa_supplicant/wpa_supplicant.conf 20 | 21 | """ 22 | 23 | location = "/etc/wpa_supplicant/wpa_supplicant.conf" 24 | 25 | template = textwrap.dedent(""" 26 | wifis: 27 | wlan0: 28 | dhcp4: true 29 | optional: true 30 | access-points: 31 | {ssid}: 32 | password: "{password}" 33 | """).strip() 34 | 35 | @staticmethod 36 | def set(ssid=None, 37 | password=None, 38 | country="US", 39 | psk=True, 40 | location=location, 41 | sudo=False): 42 | """ 43 | Sets the wifi. Only works for psk based wifi 44 | 45 | :param ssid: The ssid 46 | :type ssid: str 47 | :param password: The password 48 | :type password: str 49 | :param country: Two digit country code 50 | :type country: str 51 | :param psk: If true uses psk authentication 52 | :type psk: bool 53 | :param location: The file where the configuration file should be written to 54 | :type location: str 55 | :param sudo: If tru the write will be done with sudo 56 | :type sudo: bool 57 | :return: True if success 58 | :rtype: bool 59 | """ 60 | 61 | if ssid is None or password is None: 62 | Console.error("SSID or password not set") 63 | return False 64 | 65 | config = Wifi.template.format(**locals()) 66 | 67 | try: 68 | if sudo: 69 | Sudo.writefile(location, config) 70 | else: 71 | writefile(location, config) 72 | 73 | except FileNotFoundError as e: # noqa: F841 74 | Console.error(f"The file does not exist: {location}") 75 | return False 76 | return True 77 | -------------------------------------------------------------------------------- /deprecated/activate_ssh.py: -------------------------------------------------------------------------------- 1 | @windows_not_supported 2 | def activate_ssh(self, public_key, debug=False, interactive=False): 3 | """ 4 | Sets the public key path and copies it to the SD card 5 | 6 | TODO: this has bugs as we have not yet thought about debug, 7 | interactive, yesno yesno we can take form cloudmesh.common 8 | 9 | BUG: this just raise a non implementation error 10 | 11 | :param public_key: the public key location 12 | :type public_key: str 13 | :param debug: if set to tru debug messages will be printed 14 | :type debug: bool 15 | :param interactive: set to tru if you like interactive mode 16 | :type interactive: bool 17 | :return: True if successful 18 | :rtype: bool 19 | """ 20 | 21 | # set the keypath 22 | self.keypath = public_key 23 | if debug: 24 | print(self.keypath) 25 | if not os.path.isfile(self.keypath): 26 | Console.error("key does not exist", self.keypath) 27 | sys.exit() 28 | 29 | if self.dryrun: 30 | print("DRY RUN - skipping:") 31 | print("Activate ssh authorized_keys pkey:{}".format(public_key)) 32 | return 33 | elif interactive: 34 | if not yn_choice("About to write ssh config. Please confirm:"): 35 | return 36 | 37 | # activate ssh by creating an empty ssh file in the boot drive 38 | pathlib.Path(self.filename("/ssh")).touch() 39 | # Write the content of the ssh rsa to the authorized_keys file 40 | key = pathlib.Path(public_key).read_text() 41 | ssh_dir = self.filename("/home/pi/.ssh") 42 | print(ssh_dir) 43 | if not os.path.isdir(ssh_dir): 44 | os.makedirs(ssh_dir) 45 | auth_keys = ssh_dir / "authorized_keys" 46 | auth_keys.write_text(key) 47 | 48 | # We need to fix the permissions on the .ssh folder but it is hard to 49 | # get this working from a host OS because the host OS must have a user 50 | # and group with the same pid and gid as the raspberry pi OS. On the PI 51 | # the pi uid and gid are both 1000. 52 | 53 | # All of the following do not work on OS X: 54 | # execute("chown 1000:1000 {ssh_dir}".format(ssh_dir=ssh_dir)) 55 | # shutil.chown(ssh_dir, user=1000, group=1000) 56 | # shutil.chown(ssh_dir, user=1000, group=1000) 57 | # execute("sudo chown 1000:1000 {ssh_dir}".format(ssh_dir=ssh_dir)) 58 | 59 | # Changing the modification attributes does work, but we can just handle 60 | # this the same way as the previous chown issue for consistency. 61 | # os.chmod(ssh_dir, 0o700) 62 | # os.chmod(auth_keys, 0o600) 63 | 64 | # /etc/rc.local runs at boot with root permissions - since the file 65 | # already exists modifying it shouldn't change ownership or permissions 66 | # so it should run correctly. One lingering question is: should we clean 67 | # this up later? 68 | 69 | new_lines = textwrap.dedent(''' 70 | # FIX298-START: Fix permissions for .ssh directory 71 | if [ -d "/home/pi/.ssh" ]; then 72 | chown pi:pi /home/pi/.ssh 73 | chmod 700 /home/pi/.ssh 74 | if [ -f "/home/pi/.ssh/authorized_keys" ]; then 75 | chown pi:pi /home/pi/.ssh/authorized_keys 76 | chmod 600 /home/pi/.ssh/authorized_keys 77 | fi 78 | fi 79 | # FIX298-END 80 | ''') 81 | rc_local = self.filename("/etc/rc.local") 82 | new_rc_local = "" 83 | already_updated = False 84 | with rc_local.open() as f: 85 | for line in f: 86 | if "FIX298" in line: 87 | already_updated = True 88 | break 89 | if line == "exit 0\n": 90 | new_rc_local += new_lines 91 | new_rc_local += line 92 | else: 93 | new_rc_local += line 94 | if not already_updated: 95 | with rc_local.open("w") as f: 96 | f.write(new_rc_local) 97 | self.disable_password_ssh() 98 | -------------------------------------------------------------------------------- /deprecated/activate_ssh.py-removed: -------------------------------------------------------------------------------- 1 | @windows_not_supported 2 | def activate_ssh(self, public_key="~/.ssh/id_rsa.pub", debug=False, interactive=False): 3 | """ 4 | Sets the public key path and copies it to the SD card 5 | 6 | :param public_key: the public key location 7 | :type public_key: str 8 | :param debug: if set to tru debug messages will be printed 9 | :type debug: bool 10 | :param interactive: set to tru if you like interactive mode 11 | :type interactive: bool 12 | :return: True if successful 13 | :rtype: bool 14 | """ 15 | 16 | # file, owner, grooup 17 | files = [ 18 | ["boot/ssh", 0, 0, 777], 19 | ["boot/wpa_supplicant.conf", 0, 0, 600], 20 | ["rootfs/etc/hosts", 0, 0, 644], 21 | ["rootfs/etc/dhcpcd.conf", 0, 109, 664], 22 | ["rootfs/etc/hostname", 0, 0, 644], 23 | ["rootfs/home/pi/.ssh/authorized_keys", 1000, 1000, 644], 24 | ["rootfs/home/pi/.ssh/id_rsa", 1000, 1000, 600], 25 | ["rootfs/home/pi/.ssh/id_rsa.pub", 1000, 1000, 644] 26 | ] 27 | 28 | card = SDCard() 29 | 30 | public_key = path_expand(public_key) 31 | if not os.path.isfile(public_key): 32 | Console.error("key does not exist", public_key) 33 | return False 34 | 35 | # TODO: we likely have this already 36 | self.disable_password_ssh() 37 | 38 | # TODO: this is likely also done elsewhere 39 | # activate ssh by creating an empty ssh file in the boot drive 40 | pathlib.Path(f"{card.boot_volume}/ssh").touch() 41 | # Write the content of the ssh rsa to the authorized_keys file 42 | key = pathlib.Path(public_key).read_text() 43 | ssh_dir = f"{card.root_volume}/home/pi/.ssh" 44 | print(ssh_dir) 45 | if not os.path.isdir(ssh_dir): 46 | os.makedirs(ssh_dir) 47 | auth_keys = ssh_dir / "authorized_keys" 48 | auth_keys.write_text(key) 49 | 50 | if os.getuid() != 1000: 51 | 52 | # We need to fix the permissions on the .ssh folder but it is hard to 53 | # get this working from a host OS because the host OS must have a user 54 | # and group with the same pid and gid as the raspberry pi OS. On the PI 55 | # the pi uid and gid are both 1000. 56 | 57 | # All of the following do not work on OS X: 58 | # execute("chown 1000:1000 {ssh_dir}".format(ssh_dir=ssh_dir)) 59 | # shutil.chown(ssh_dir, user=1000, group=1000) 60 | # shutil.chown(ssh_dir, user=1000, group=1000) 61 | # execute("sudo chown 1000:1000 {ssh_dir}".format(ssh_dir=ssh_dir)) 62 | 63 | # Changing the modification attributes does work, but we can just handle 64 | # this the same way as the previous chown issue for consistency. 65 | # os.chmod(ssh_dir, 0o700) 66 | # os.chmod(auth_keys, 0o600) 67 | 68 | # /etc/rc.local runs at boot with root permissions - since the file 69 | # already exists modifying it shouldn't change ownership or permissions 70 | # so it should run correctly. Is there a better solution? 71 | # after the first boot, I think this should be be removed? 72 | # 73 | new_lines = textwrap.dedent(''' 74 | # FIX298-START: Fix permissions for .ssh directory 75 | if [ -d "/home/pi/.ssh" ]; then 76 | chown pi:pi /home/pi/.ssh 77 | chmod 700 /home/pi/.ssh 78 | if [ -f "/home/pi/.ssh/authorized_keys" ]; then 79 | chown pi:pi /home/pi/.ssh/authorized_keys 80 | chmod 600 /home/pi/.ssh/authorized_keys 81 | fi 82 | fi 83 | # FIX298-END 84 | ''') 85 | 86 | rc_local = f"{card.root_volume}/etc/rc.local" 87 | new_rc_local = "" 88 | already_updated = False 89 | with rc_local.open() as f: 90 | for line in f: 91 | if "FIX298" in line: 92 | already_updated = True 93 | break 94 | if line == "exit 0\n": 95 | new_rc_local += new_lines 96 | new_rc_local += line 97 | else: 98 | new_rc_local += line 99 | if not already_updated: 100 | with rc_local.open("w") as f: 101 | f.write(new_rc_local) 102 | return True 103 | -------------------------------------------------------------------------------- /deprecated/admin.py-removed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # (C) COPYRIGHT © Preston Landers 2010 4 | # Released under the same license as Python 2.6.5 5 | 6 | # see: https://github.com/dlcowen/dfirwizard/blob/master/admin.py 7 | 8 | import os 9 | import traceback 10 | import types 11 | 12 | import sys 13 | 14 | 15 | def isUserAdmin(): 16 | if os.name == 'nt': 17 | import ctypes 18 | # WARNING: requires Windows XP SP2 or higher! 19 | try: 20 | return ctypes.windll.shell32.IsUserAnAdmin() 21 | except: 22 | traceback.print_exc() 23 | print("Admin check failed, assuming not an admin.") 24 | return False 25 | elif os.name == 'posix': 26 | # Check for root on Posix 27 | return os.getuid() == 0 28 | else: 29 | raise RuntimeError( 30 | "Unsupported operating system for this module: %s" % (os.name,)) 31 | 32 | 33 | def runAsAdmin(cmdLine=None, wait=True): 34 | if os.name != 'nt': 35 | raise RuntimeError("This function is only implemented on Windows.") 36 | 37 | import win32con, win32event, win32process 38 | from win32com.shell.shell import ShellExecuteEx 39 | from win32com.shell import shellcon 40 | 41 | python_exe = sys.executable 42 | 43 | if cmdLine is None: 44 | cmdLine = [python_exe] + sys.argv 45 | elif type(cmdLine) not in (types.TupleType, types.ListType): 46 | raise ValueError("cmdLine is not a sequence.") 47 | cmd = '"%s"' % (cmdLine[0],) 48 | # XXX TODO: isn't there a function or something we can call 49 | # to massage command line params? 50 | params = " ".join(['"%s"' % (x,) for x in cmdLine[1:]]) 51 | cmdDir = '' 52 | showCmd = win32con.SW_SHOWNORMAL 53 | # showCmd = win32con.SW_HIDE 54 | lpVerb = 'runas' # causes UAC elevation prompt. 55 | 56 | # print "Running", cmd, params 57 | 58 | # ShellExecute() doesn't seem to allow us to fetch the PID or handle 59 | # of the process, so we can't get anything useful from it. Therefore 60 | # the more complex ShellExecuteEx() must be used. 61 | 62 | # procHandle = win32api.ShellExecute(0, lpVerb, cmd, 63 | # params, cmdDir, showCmd) 64 | 65 | procInfo = ShellExecuteEx(nShow=showCmd, 66 | fMask=shellcon.SEE_MASK_NOCLOSEPROCESS, 67 | lpVerb=lpVerb, 68 | lpFile=cmd, 69 | lpParameters=params) 70 | 71 | if wait: 72 | procHandle = procInfo['hProcess'] 73 | obj = win32event.WaitForSingleObject(procHandle, win32event.INFINITE) 74 | rc = win32process.GetExitCodeProcess(procHandle) 75 | # print "Process handle %s returned code %s" % (procHandle, rc) 76 | else: 77 | rc = None 78 | 79 | return rc 80 | 81 | 82 | def test(): 83 | rc = 0 84 | if not isUserAdmin(): 85 | print("You're not an admin.", os.getpid(), "params: ", sys.argv) 86 | # rc = runAsAdmin(["c:\\Windows\\notepad.exe"]) 87 | rc = runAsAdmin() 88 | else: 89 | print("You are an admin!", os.getpid(), "params: ", sys.argv) 90 | rc = 0 91 | # x = raw_input('Press Enter to exit.') 92 | return rc 93 | -------------------------------------------------------------------------------- /deprecated/card/pi.py: -------------------------------------------------------------------------------- 1 | class CardOnPi: 2 | 3 | def mount(self): 4 | pass 5 | 6 | def unmount(self): 7 | pass 8 | 9 | def formad_card(self): 10 | pass 11 | 12 | def writefile(self): 13 | pass 14 | 15 | def readfile(self): 16 | pass 17 | -------------------------------------------------------------------------------- /deprecated/cloudinit.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from cloudmesh.common.Host import Host 4 | from cloudmesh.common.parameter import Parameter 5 | 6 | # 7 | # carefull apt get requires network is working via master 8 | # 9 | 10 | # see also https://github.com/number5/cloud-init/tree/master/doc/examples 11 | 12 | 13 | class Cloudinit: 14 | 15 | def __init__(self): 16 | self.content = [] 17 | self.runcmd = [] 18 | self.user = [] 19 | self.packages = [] # not yet sure how to do that 20 | 21 | # runcmd must be at end and only run once 22 | 23 | def write(self, filename=None): 24 | raise NotImplementedError 25 | # 26 | # writes to mounted boot, gets it from SDCard() 27 | # 28 | 29 | def update(self, reboot=False): 30 | commands = f""" 31 | apt_update: true 32 | apt_upgrade: true 33 | package_reboot_if_required: {reboot} 34 | """ 35 | # reboot must be lower 36 | self.add("update and upgrade", commands) 37 | 38 | def reboot(self, delay): 39 | commands = f""" 40 | power_state: 41 | delay: '+{delay}' 42 | mode: reboot 43 | message: Reboot 44 | """ 45 | self.add("reboot", commands) 46 | 47 | def upstart(self): 48 | upstart = """ 49 | #upstart-job 50 | description "My test job" 51 | 52 | start on cloud-config 53 | console output 54 | task 55 | 56 | script 57 | echo "====BEGIN=======" 58 | echo "HELLO WORLD: $UPSTART_JOB" 59 | echo "=====END========" 60 | end script 61 | """ 62 | self.add("upstart", upstart) 63 | 64 | def get(self): 65 | users = "" 66 | runcmd = "" 67 | content = "" 68 | packages = "" 69 | final_message = "final_message: Cloudmesh configured your system, after $UPTIME seconds" 70 | 71 | if len(self.runcmd) > 0: 72 | packages = "\npackages:\n" + " - " + "\n - ".join(self.packages) 73 | 74 | if len(self.runcmd) > 0: 75 | runcmd = "\nruncmd:\n" + " - " + "\n - ".join(self.runcmd) 76 | 77 | if len(self.user) > 0: 78 | users = textwrap.dedent(""" 79 | users: 80 | """ + " - ".join(self.user)) 81 | 82 | if len(self.content) > 0: 83 | content = "\n".join(self.content) 84 | 85 | return content + users + packages + runcmd + final_message 86 | 87 | def __str__(self): 88 | return self.get() 89 | 90 | def __repr__(self): 91 | return self.get() 92 | 93 | def add(self, what, content): 94 | comment = f"#\n# Set {what}\n#\n" 95 | self.content.append(comment + textwrap.dedent(content).strip()) 96 | 97 | def register(self): 98 | """ 99 | not yet implemented 100 | :return: 101 | :rtype: 102 | """ 103 | phone = textwrap.dedent(""" 104 | phone_home: 105 | url: http://my.example.com/$INSTANCE_ID/ 106 | post: [ pub_key_dsa, pub_key_rsa, pub_key_ecdsa, instance_id ] 107 | """) 108 | self.add("register to inventory", phone) 109 | 110 | def wifi(self): 111 | content = """ 112 | # This file is generated from information provided by 113 | # the datasource. Changes to it will not persist across an instance. 114 | # To disable cloud-init's network configuration capabilities, write a file 115 | # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: 116 | # network: {config: disabled} 117 | network: 118 | version: 2 119 | ethernets: 120 | eth0: 121 | optional: true 122 | dhcp4: true 123 | # add wifi setup information here ... 124 | wifis: 125 | wlan0: 126 | optional: true 127 | access-points: 128 | "YOUR-SSID-NAME": 129 | password: "YOUR-NETWORK-PASSWORD" 130 | dhcp4: true 131 | """ 132 | self.add("wifi", content) 133 | 134 | def static_network(self, *, hostnames, ips): 135 | content = """ 136 | network-interfaces: | 137 | auto eth0 138 | iface eth0 inet static 139 | address 192.168.1.10 140 | network 192.168.1.0 141 | netmask 255.255.255.0 142 | broadcast 192.168.1.255 143 | gateway 192.168.1.1 144 | """ 145 | self.add("static network addresses", content) 146 | 147 | def nameserver(self): 148 | content = """ 149 | manage_resolv_conf: true 150 | resolv_conf: 151 | nameservers: ['8.8.4.4', '8.8.8.8'] 152 | searchdomains: 153 | - foo.example.com 154 | - bar.example.com 155 | domain: example.com 156 | options: 157 | rotate: true 158 | timeout: 1 159 | """ 160 | self.add("name server", content) 161 | 162 | def ntp(self): 163 | content = """ 164 | ntp: 165 | servers: 166 | - ntp1.example.com 167 | - ntp2.example.com 168 | - ntp3.example.com 169 | runcmd: 170 | - /usr/bin/systemctl enable --now ntpd 171 | """ 172 | self.add("ntp", content) 173 | 174 | def dhcp(self): 175 | # not sure how to dod this 176 | # we may not need immediatly as we do static 177 | raise NotImplementedError 178 | 179 | def keyboard(self): 180 | self.runcmd.append( 181 | "/usr/bin/localectl set-keymap de-latin1-nodeadkeys" 182 | ) 183 | 184 | def locale(self, region="en_US.UTF-8"): 185 | # see keyboaad and timezone 186 | # may need consideration for wifi country 187 | # not yet sure if we eneed this as it may just be done as 188 | # part of the 189 | self.content.append("locale: en_US.UTF-8") 190 | self.content.append("locale_configfile: /etc/default/locale") 191 | 192 | def hostname(self, name): 193 | content = f""" 194 | preserve_hostname: false 195 | hostname: {name} 196 | """ 197 | self.add("hostname", content) 198 | 199 | def etc_hosts(self): 200 | content = """ 201 | write_files: 202 | - path: /etc/hosts 203 | permissions: '0644' 204 | content: | 205 | #Host file 206 | 127.0.0.1 localhost localhost.localdomain 207 | 208 | 10.252.0.1 vm0-ib0 209 | 10.252.0.2 vm1-ib0 210 | 10.252.0.3 vm2-ib1 211 | """ 212 | self.add("/etc/hosts", content) 213 | 214 | def startup(self): 215 | # not sure if we need that 216 | raise NotImplementedError 217 | 218 | def set_key(self): 219 | # set key wudl be the host key? 220 | raise NotImplementedError 221 | 222 | def add_key(self): 223 | content = """ 224 | # 225 | # Add keys 226 | # 227 | 228 | ssh_deletekeys: False 229 | ssh_pwauth: True 230 | ssh_authorized_keys: 231 | - ssh-rsa XXXKEY mail@example.com 232 | """ 233 | self.add("keys", content) 234 | 235 | def add_user(self, *, name, gecos, group, groups, expire, passwd): 236 | """ 237 | 238 | :param self: 239 | :type self: 240 | :param name: 241 | :type name: 242 | :param gecos: Firstname Lastname 243 | :type gecos: str 244 | :param group: 245 | :type group: 246 | :param groups: 247 | :type groups: 248 | :param expire: 249 | :type expire: 250 | :param passwd: 251 | :type passwd: 252 | :return: 253 | :rtype: 254 | """ 255 | user = f""" 256 | name: {name} 257 | gecos: {gecos} 258 | primary_group: {group} 259 | groups: {groups} 260 | # selinux_user: staff_u 261 | # expiredate: '2012-09-01' 262 | # ssh_import_id: foobar 263 | lock_passwd: false 264 | passwd: {passwd} 265 | """ 266 | self.user.append(user) 267 | 268 | def enable_ssh(self): 269 | # apt install need to be done differently 270 | # ther is a special section for that 271 | self.packages.append("openssh-server") # may already be installed, s we ma not need this 272 | ssh = textwrap.dedent(""" 273 | sudo systemctl enable --now ssh 274 | sudo ufw allow ssh 275 | sudo ufw enable 276 | """).strip() 277 | for line in ssh.splitlines(): 278 | self.runcmd.append(line) 279 | 280 | def disable_password(self): 281 | raise NotImplementedError 282 | 283 | def configure_manager(self): 284 | raise NotImplementedError 285 | # use the calls above for a single easy to use method 286 | 287 | def configure_worker(self): 288 | raise NotImplementedError 289 | # use the calls above for a single easy to use method 290 | 291 | # 292 | # POST INSTALATION 293 | # 294 | def firmware(self): 295 | raise NotImplementedError 296 | 297 | 298 | if __name__ == "__main__": 299 | hostnames = Parameter.expand("red,red[01-02]") 300 | ips = Parameter.expand("10.0.0.[1-3]") 301 | manager, workers = Host.get_hostnames(hostnames) 302 | 303 | cloudinit = Cloudinit() 304 | cloudinit.hostname(manager) 305 | cloudinit.etc_hosts() # manager, workers, IPS, worker 306 | cloudinit.update() 307 | cloudinit.keyboard() # locale as parameter 308 | cloudinit.enable_ssh() 309 | # cloudinit.register() 310 | print("cloudinit") 311 | # 312 | # ADD WHAT IS NEEDED 313 | print(cloudinit) 314 | -------------------------------------------------------------------------------- /deprecated/detect.py-removed: -------------------------------------------------------------------------------- 1 | @windows_not_supported 2 | def detect(self): 3 | """ 4 | Detects if a USB card writer can be found. and just prints the result 5 | """ 6 | # Clear dmesg table so that info doesn't get confused with previous detects 7 | self.system('sudo dmesg -c') 8 | banner("Detecting USB Card Writers(s)") 9 | 10 | print("Make sure the USB Writers(s) is removed ...") 11 | if not yn_choice("Is the writer(s) removed?"): 12 | sys.exit() 13 | usb_out = set(Shell.execute("lsusb").splitlines()) 14 | print("Now plug in the Writer(s) ...") 15 | if not yn_choice("Is the writer(s) plugged in?"): 16 | sys.exit() 17 | usb_in = set(Shell.execute("lsusb").splitlines()) 18 | 19 | writer = usb_in - usb_out 20 | if len(writer) == 0: 21 | print( 22 | "ERROR: we did not detect the device, make sure it is plugged.") 23 | sys.exit() 24 | else: 25 | banner("Detected Card Writers") 26 | 27 | print("\n".join(writer)) 28 | print() 29 | -------------------------------------------------------------------------------- /deprecated/dmesg-usb: -------------------------------------------------------------------------------- 1 | ''' 2 | # print (devices) 3 | details = [] 4 | for device in devices: 5 | name = os.path.basename(device) 6 | dmesg = result = subprocess.getoutput(command) 7 | _fdisk = USB.fdisk(name) 8 | result = result.replace("Write Protect is", "write_protection:") 9 | result = result.replace("Attached SCSI removable disk", 10 | "removable_disk: True") 11 | result = result.replace("(", "") 12 | result = result.replace(")", "") 13 | result = result.splitlines() 14 | _dmesg = [x.split(f"[{name}]", 1)[1].strip() for x in result] 15 | size = _get_attribute("logical blocks:", _dmesg) 16 | if size is not None: 17 | details.append({ 18 | 'dmesg': dmesg, 19 | 'fdisk': _fdisk, 20 | 'name': name, 21 | 'dev': device, 22 | 'removable_disk': _get_attribute("removable_disk:", _dmesg), 23 | 'write_protection': \ 24 | _get_attribute("write_protection:", 25 | _dmesg) is not "off", 26 | 'size': size, 27 | }) 28 | return details 29 | ''' 30 | -------------------------------------------------------------------------------- /deprecated/info.py: -------------------------------------------------------------------------------- 1 | @windows_not_supported 2 | def info(self, 3 | print_os=True, 4 | print_fdisk=True, 5 | print_stdout=True, 6 | output="table"): 7 | """ 8 | Finds out information about USB devices 9 | 10 | TODO: should we rename print_stdout to debug? seems more in 11 | line with cloudmesh 12 | 13 | :param print_os: 14 | :type print_os: 15 | :param print_fdisk: 16 | :type print_fdisk: 17 | :param print_stdout: if set to True prints debug information 18 | :type print_stdout: bool 19 | :param output: 20 | :type output: 21 | :return: dict with details about the devices 22 | :rtype: dict 23 | """ 24 | 25 | if print_os and print_stdout: 26 | if os_is_pi(): 27 | banner("This is Raspberry PI") 28 | elif os_is_mac(): 29 | banner("This is Mac") 30 | elif os_is_windows(): 31 | banner("This is a Windows Computer") 32 | elif os_is_linux(): 33 | banner("This is a Linux Computer") 34 | else: 35 | Console.error("unkown OS") 36 | sys.exit(1) 37 | 38 | if os_is_pi() and print_fdisk and print_stdout: 39 | result = USB.fdisk("/dev/mmcblk0") 40 | if print_stdout: 41 | banner("Operating System SD Card") 42 | print(result) 43 | 44 | details = USB.get_from_usb() 45 | 46 | if print_stdout and details is not None: 47 | banner("USB Device Probe") 48 | print(Printer.write( 49 | details, 50 | order=["address", 51 | "bus", 52 | "idVendor", 53 | "idProduct", 54 | "hVendor", 55 | "hProduct", 56 | "iManufacturer", 57 | "iSerialNumber", 58 | "usbVersion", 59 | "comment"], 60 | header=["Adr.", 61 | "bus", 62 | "Vendor", 63 | "Prod.", 64 | "H Vendor", 65 | "H Prod.", 66 | "Man.", 67 | "Ser.Num.", 68 | "USB Ver.", 69 | "Comment"], 70 | output=output) 71 | ) 72 | 73 | # devices = USB.get_devices() 74 | 75 | # banner("Devices found") 76 | 77 | # print ('\n'.join(sorted(devices))) 78 | 79 | if os_is_mac(): 80 | 81 | names = USB.get_dev_from_diskutil() 82 | 83 | details = USB.get_from_diskutil() 84 | else: 85 | details = USB.get_from_dmesg() 86 | 87 | if print_stdout: 88 | banner("SD Cards Found") 89 | 90 | if os_is_mac(): 91 | print("We found the follwing cards:") 92 | print(" - /dev/" + "\n - /dev/".join(names)) 93 | print() 94 | print("We found the follong file systems on these disks:") 95 | print() 96 | 97 | print(Printer.write(details, 98 | order=[ 99 | "dev", 100 | "info", 101 | "formatted", 102 | "size", 103 | "active", 104 | "readable", 105 | "empty", 106 | "direct-access", 107 | "removable", 108 | "writeable"], 109 | header=[ 110 | "Path", 111 | "Info", 112 | "Formatted", 113 | "Size", 114 | "Plugged-in", 115 | "Readable", 116 | "Empty", 117 | "Access", 118 | "Removable", 119 | "Writeable"], 120 | output=output) 121 | ) 122 | 123 | # lsusb = USB.get_from_lsusb() 124 | # from pprint import pprint 125 | # pprint (lsusb) 126 | 127 | # endors = USB.get_vendor() 128 | # print(vendors) 129 | 130 | # udev = subprocess.getoutput("udevadm info -a -p $(udevadm info -q path -n /dev/sda)") 131 | # 132 | # attributes = ["vendor","model", "model", "version", "manufacturer", 133 | # "idProduct", "idVendor"] 134 | # for line in udev.splitlines(): 135 | # if any(word in line for word in attributes): 136 | # print(line) 137 | 138 | if print_stdout: 139 | 140 | if os_is_linux(): 141 | card = SDCard() 142 | m = card.ls() 143 | 144 | banner("Mount points") 145 | if len(m) != 0: 146 | print(Printer.write(m, 147 | order=["name", "path", "type", "device", "parameters"], 148 | header=["Name", "Path", "Type", "Device", "Parameters"], 149 | output=output)) 150 | else: 151 | Console.warning("No mount points found. Use cms burn mount") 152 | print() 153 | 154 | # Convert details into a dict where the key for each entry is the device 155 | details = {detail['dev']: detail for detail in details} 156 | 157 | return details 158 | -------------------------------------------------------------------------------- /deprecated/parts.py: -------------------------------------------------------------------------------- 1 | import oyaml as yaml 2 | import sys 3 | from cloudmesh.common.util import readfile 4 | import pandas as pd 5 | 6 | pd.options.display.float_format = '{:,.2f}'.format 7 | 8 | try: 9 | file = sys.argv[1] 10 | except: 11 | file = "README-parts-list.yml" 12 | 13 | content = readfile(file) 14 | 15 | content = yaml.safe_load(content) 16 | 17 | data = {} 18 | for i in range(0,len(content)): 19 | data[i] = content[i] 20 | 21 | 22 | 23 | df = pd.DataFrame( 24 | data=data, 25 | index=["vendor", 26 | "description", 27 | "count", 28 | "price", 29 | "comment", 30 | "other"] 31 | 32 | ).transpose() 33 | 34 | df["total"] = df["count"] * df["price"] 35 | total = df["total"].sum() 36 | 37 | 38 | 39 | 40 | line = {} 41 | for col in content[0]: 42 | if col == "total": 43 | line[col] = "========" 44 | else: 45 | line[col] = "" 46 | total_line = dict(line) 47 | total_line["total"] = total 48 | 49 | print (line) 50 | 51 | 52 | # df = df.append(line,ignore_index=True) 53 | df = df.append(total_line,ignore_index=True) 54 | 55 | 56 | print(df[["vendor", 57 | "description", 58 | "count", 59 | "price", 60 | "total", 61 | "comment", 62 | "other", 63 | "url"]].to_markdown()) 64 | 65 | -------------------------------------------------------------------------------- /deprecated/report-reu.md: -------------------------------------------------------------------------------- 1 | # REU on creating a compute analysis cluster from Raspberry PIs 2 | 3 | Gregor von Laszewski, laszewski@gmail.com 4 | 5 | ## Abstract 6 | 7 | TBD 8 | 9 | ## Introduction 10 | 11 | 12 | ## Setting up a Cluster with Raspbian 13 | 14 | ### cm-pi-burn 15 | 16 | describe high level what it does nad how we use it 17 | 18 | ### Alternatives to cm-pi-burn 19 | 20 | PXE booting. we will focus first on cmpi burn 21 | 22 | ## Setting up Kubernetes on the Cluster 23 | 24 | ## Example use of the Kubernetes Cluster 25 | 26 | ## Benchmark 27 | 28 | ## Conclusion 29 | 30 | 31 | ## Tasks 32 | 33 | * send info to Gregor 34 | * get in payrol to get payed 35 | * take building seminar for after hour access 36 | * get familiar with sdcard burn from a pi, you may have to burn one on your laptop first, remember to change passwd before setting it on network 37 | * get familiar with cm-pi-brn 38 | * read Subs report 39 | * make sure to use branches when working on the code. 40 | 41 | * start building a cluster 42 | 43 | ### Hardware 44 | 45 | * get spacers (which size?) 46 | * make small inventory count, PI3 and Pi3B+ 47 | * we will get new network switch and PI4 for masters 48 | * figure out if we need anything else 49 | 50 | 51 | ## Refernces 52 | 53 | * Proceedings that includes previous work: 54 | * Github repo: 55 | * Previous work: 56 | * Pi Book, Gregor von Laszewski 57 | 58 | 59 | -------------------------------------------------------------------------------- /deprecated/staticip: -------------------------------------------------------------------------------- 1 | # TODO: 2 | # Deprecated function as dhcpcd.conf is the recommended file for 3 | # configuring static network configs. Should we keep this? 4 | # 5 | # def set_static_ip2(self, ip, mountpoint, iface="eth0", mask="16"): 6 | # """ 7 | # Sets the static ip on the sd card for the specified interface 8 | # Also writes to manager hosts file for easy access 9 | 10 | # :param ip: ips address 11 | # :type ip: str 12 | # :param mountpoint: the mountpunt of the device on which the ip 13 | # is found 14 | # :type mountpoint: str 15 | # :param iface: the network Interface 16 | # :type iface: str 17 | # :param mask: the subnet Mask 18 | # :type mask: str 19 | # :return: 20 | # :rtype: 21 | # """ 22 | 23 | # # Adds the ip and hostname to /etc/hosts if it isn't already there. 24 | # def add_to_hosts(ip): 25 | # # with open('/etc/hosts', 'r') as host_file: 26 | # # hosts = host_file.readlines() 27 | # hosts = SDCard.readfile('/etc/hosts', decode=True) 28 | 29 | # replaced = False 30 | # for i in range(len(hosts)): 31 | # ip_host = hosts[i].split() 32 | 33 | # if len(ip_host) > 1: 34 | # if ip_host[0] == ip: 35 | # ip_host[1] = self.hostname 36 | # hosts[i] = f"{ip_host[0]}\t{ip_host[1]}\n" 37 | # replaced = True 38 | 39 | # elif ip_host[1] == self.hostname: 40 | # ip_host[0] = ip 41 | # hosts[i] = f"{ip_host[0]}\t{ip_host[1]}\n" 42 | # replaced = True 43 | # if not replaced: 44 | # hosts.append(f"{ip}\t{self.hostname}\n") 45 | 46 | # # with open('/etc/hosts', 'w') as host_file: 47 | # # host_file.writelines(hosts) 48 | # config = "" 49 | # for line in hosts: 50 | # config = config + line + '\n' 51 | 52 | # SDCard.writefile('/etc/hosts', config) 53 | 54 | # # Add static IP and hostname to manager's hosts file and configure worker with static IP 55 | # if not self.dryrun: 56 | # add_to_hosts(ip) 57 | 58 | # # Configure static LAN IP 59 | # if iface == "eth0": 60 | # interfaces_conf = textwrap.dedent(f""" 61 | # auto {iface} 62 | # iface {iface} inet static 63 | # address {ip}/{mask} 64 | # """) 65 | # # with open(f'{mountpoint}/etc/network/interfaces', 66 | # # 'a') as config: 67 | # # config.write(interfaces_conf) 68 | # SDCard.writefile(f'{mountpoint}/etc/network/interfaces', 69 | # interfaces_conf, append=True) 70 | 71 | # # Configure static wifi IP 72 | # elif iface == "wlan0": 73 | # dnss = \ 74 | # self.system_exec_exec("cat /etc/resolv.conf | grep nameserver").split()[ 75 | # 1] # index 0 is "nameserver" so ignore 76 | # routerss = self.system_exec_exec( 77 | # "ip route | grep default | awk '{print $3}'") # omit the \n at the end 78 | # dhcp_conf = textwrap.dedent(f""" 79 | # interface wlan0 80 | # static ip_address={ip} 81 | # static routers={routerss} 82 | # static domain_name_servers={dnss} 83 | # """) 84 | # # with open(f'{mountpoint}/etc/dhcpcd.conf', 'a') as config: 85 | # # config.write(dhcp_conf) 86 | # SDCard.writefile(f'{mountpoint}/etc/dhcpcd.conf', dhcp_conf, 87 | # append=True) 88 | # else: 89 | # print('interface eth0\n') 90 | # print(f'static ip_address={ip}/{mask}') 91 | -------------------------------------------------------------------------------- /dev-scripts/TestNotebook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # In[1]: 5 | import sys 6 | 7 | # from cloudmesh.burn.windowssdcard import WindowsSDCard 8 | import time 9 | from cloudmesh.common.systeminfo import os_is_windows 10 | 11 | import string 12 | from cloudmesh.common.console import Console 13 | from cloudmesh.common.Shell import Shell 14 | from cloudmesh.common.util import writefile as common_writefile 15 | from cloudmesh.common.util import readfile as common_readfile 16 | from cloudmesh.common.util import yn_choice 17 | from cloudmesh.common.util import path_expand 18 | from pathlib import Path 19 | import os 20 | import sys 21 | import string 22 | import subprocess 23 | import re 24 | from pathlib import Path 25 | 26 | # In[2]: 27 | 28 | class WindowsSDCard: 29 | tmp = "tmp.txt" 30 | 31 | def __init__(self): 32 | pass 33 | 34 | def assign_drive(self,volume=None,drive=None): 35 | if drive is None: 36 | drive = self.guess_drive() 37 | result = self.diskpart(f"select volume {volume}\nassign letter={drive}") 38 | return result 39 | 40 | def info(self): 41 | 42 | b = self.diskpart("list volume") 43 | volumes = self.process_volumes_text(text=b) 44 | 45 | d = volumes[0]["drive"] 46 | v = volumes[0]["volume"] 47 | 48 | drive_assigned = d != "" 49 | if not drive_assigned: 50 | pass 51 | #assign the drive letter 52 | self.assign_drive(volume=v,drive) 53 | #find more info for sdb 54 | #self.remove_letter() 55 | 56 | else: 57 | pass 58 | #find more info 59 | 60 | 61 | return volumes 62 | 63 | def process_volumes_text(self,text=None): 64 | if text is None: 65 | Console.error("No volume info provided") 66 | else: 67 | lines = text.splitlines() 68 | result = [] 69 | for line in lines: 70 | if "Removable" in line and "Healthy" in line: 71 | result.append(line) 72 | 73 | content = [] 74 | for line in result: 75 | data = { 76 | 77 | "volume": line[0:13].replace("Volume", "").strip(), 78 | "drive": line[13:18].strip(), 79 | "label": line[18:31].strip(), 80 | "fs": line[31:38].strip(), 81 | "type": line[38:50].strip(), 82 | "size": line[50:59].strip(), 83 | "status": line[59:70].strip(), 84 | "info": line[70:].strip() 85 | 86 | } 87 | content.append(data) 88 | 89 | return content 90 | 91 | 92 | def diskpart(self, command): 93 | _diskpart = Path("C:/Windows/system32/diskpart.exe") 94 | common_writefile(WindowsSDCard.tmp, f"{command}\nexit") 95 | b = Shell.run(f"{_diskpart} /s {WindowsSDCard.tmp}") 96 | WindowsSDCard.clean() 97 | return b 98 | 99 | @staticmethod 100 | def clean(): 101 | os.remove(WindowsSDCard.tmp) 102 | 103 | card = WindowsSDCard() 104 | print(card.info()) 105 | 106 | '''card = WindowsSDCard() 107 | def Info(): 108 | content = card.info() 109 | content2 = card.device_info() 110 | print(content) 111 | print(content2) 112 | 113 | 114 | # In[3]: 115 | 116 | 117 | # card.inject(volume="5") 118 | 119 | 120 | # In[4]: 121 | 122 | 123 | # card.unmount(drive="D") 124 | 125 | 126 | # In[5]: 127 | 128 | 129 | # card.format_drive(drive="D") 130 | 131 | 132 | # In[6]: 133 | 134 | 135 | # card.assign_drive(volume="5") 136 | 137 | 138 | # User buys card 139 | # cards are empty and formatted with fat32 140 | # 141 | # we need to put the card in 142 | # test if the letter is there (table with all three values in there vol drive device) 143 | # we need to format the card no matter what is on there 144 | 145 | # In[7]: 146 | 147 | 148 | content = card.info() 149 | print(content) 150 | d = content[0]["drive"] 151 | v = content[0]["volume"] 152 | l = content[0]["label"] 153 | if d == "": 154 | drive = card.guess_drive() 155 | card.assign_drive(volume=v,drive=drive) 156 | content = card.info() 157 | print(content) 158 | 159 | 160 | # In[8]: 161 | 162 | 163 | card.diskpart(command=f"select volume {v}\nonline volume") 164 | #card.mount(label=l) 165 | 166 | 167 | # In[9]: 168 | 169 | 170 | content = card.info() 171 | print(content) 172 | 173 | 174 | # In[10]: 175 | 176 | 177 | device = card.filter_info(card.device_info(),{'win-mounts':d}) 178 | success = card.format_drive(drive=d) 179 | if not success: 180 | print("Exited with Error ++++++++++++++++++++") 181 | sys.exit() 182 | 183 | # we need to unmount the card so we can burn (which removes drive letter) 184 | 185 | # In[11]: 186 | 187 | 188 | device = card.filter_info(card.device_info(),{'win-mounts':d}) 189 | Info() 190 | print(device) 191 | dev = "/dev/" + device[0]["name"][0:3] 192 | print(dev) 193 | 194 | 195 | # In[12]: 196 | 197 | 198 | card.unmount(drive=d) 199 | Info() 200 | 201 | 202 | # In[18]: 203 | 204 | 205 | image_path = "/c/Users/venkata/.cloudmesh/cmburn/images/2021-03-04-raspios-buster-armhf-lite.img" 206 | os.system(f"ls {image_path}") 207 | # card.dd(image_path=image_path,device=dev) 208 | command = f"dd bs=4M if={image_path} oflag=direct of={dev} conv=fdatasync iflag=fullblock status=progress" 209 | print(command) 210 | os.system(command) 211 | print("done") 212 | 213 | 214 | # In[16]: 215 | 216 | 217 | 218 | 219 | # We need to do dd burn with image_path 220 | # ? 221 | # We want to test if card has been properly written 222 | # We eject the card 223 | # We take the card out 224 | # 225 | # release drive letter 226 | # repeat for second card 227 | 228 | # In[ ]: 229 | 230 | 231 | 232 | 233 | 234 | # In[ ]: 235 | 236 | 237 | card.device_info() 238 | 239 | 240 | # In[ ]: 241 | 242 | 243 | card.info() 244 | 245 | ''' 246 | -------------------------------------------------------------------------------- /dev-scripts/a.sh: -------------------------------------------------------------------------------- 1 | # dd bs=4M if=/c/Users/venkata/.cloudmesh/cmburn/images/2021-03-04-raspios-buster-armhf-lite.img of=/dev/sdb conv=fdatasync status=progress 2 | # command = f"sudo dd if={image_path} bs={blocksize} |" \ 3 | # f' tqdm --bytes --total {size} --ncols 80 |' \ 4 | # f" sudo dd of={device} bs={blocksize}" 5 | # else: 6 | # # command = f"sudo dd if={image_path} of={device} bs={blocksize} status=progress conv=fsync" 7 | # command = f"sudo dd if={image_path} bs={blocksize} oflag=direct |" \ 8 | # f' tqdm --bytes --total {size} --ncols 80 |' \ 9 | # f" sudo dd of={device} bs={blocksize} iflag=fullblock " \ 10 | # f"oflag=direct conv=fsync" 11 | 12 | DVC_IGNORE_ISATTY=true 13 | FILE=/c/Users/blue/.cloudmesh/cmburn/images/2021-05-07-raspios-buster-armhf-lite.img 14 | SIZE=`stat --print="%s" $FILE` 15 | DEV=/dev/sdc 16 | 17 | echo $SIZE 18 | echo $FILE 19 | 20 | dd bs=4M if=$FILE oflag=direct | 21 | tqdm --desc="format" --bytes --total=$SIZE --ncols=80 | \ 22 | dd bs=4M of=$DEV conv=fsync oflag=direct iflag=fullblock 23 | 24 | #dd bs=4M if=$FILE oflag=direct | dd of=$DEV bs=4M status=progress conv=fsync oflag=direct iflag=fullblock 25 | -------------------------------------------------------------------------------- /dev-scripts/ccc.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cloudmesh.common.util import path_expand 3 | from cloudmesh.common.util import readfile 4 | from cloudmesh.burn.raspberryos.cmdline import Cmdline 5 | from passlib.hash import sha256_crypt 6 | 7 | import sys 8 | import io 9 | from passlib.utils import pbkdf2 10 | import binascii, sys 11 | 12 | # USE 13 | # python ccc.py red SSID WIFIPASSWD PASSWD format 14 | # 15 | 16 | def psk_encrypt(ssid, password): 17 | value = pbkdf2.pbkdf2(str.encode(password), str.encode(ssid), 4096, 32) 18 | return binascii.hexlify(value).decode("utf-8") 19 | 20 | 21 | #def encrypt_user_passwd(passwd): 22 | # hash = sha512_crypt.encrypt(passwd) 23 | # return hash 24 | 25 | def encrypt_user_passwd(password): 26 | # repeatable salt if needed for testing 27 | # hash = sha256_crypt.using(salt='qY2oeR.YpL', rounds=5000).hash( 28 | # self.password) 29 | hash = sha256_crypt.using(rounds=5000).hash(password) 30 | return hash 31 | 32 | letter = "f" 33 | disk = "4" 34 | host = sys.argv[1] 35 | ssid = sys.argv[2] 36 | wifi_password = sys.argv[3] 37 | user_password = sys.argv[4] 38 | try: 39 | with_format = sys.argv[5] == "format" 40 | except: 41 | with_format = False 42 | 43 | encrypted_wifi_password = psk_encrypt(ssid, wifi_password) 44 | 45 | hash = encrypt_user_passwd(user_password) 46 | print(hash) 47 | 48 | # echo "$FIRSTUSER:"'{hash}' | chpasswd -e 49 | # fstring needs to be done carefully ....as there s { in it needs to be masked 50 | 51 | firstrun = \ 52 | f''' 53 | #!/bin/bash 54 | 55 | set +e 56 | 57 | CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \\t\\n\\r"` 58 | echo red >/etc/hostname 59 | sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t{host}/g" /etc/hosts 60 | FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 61 | FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 62 | echo "$FIRSTUSER:"'{hash}' | chpasswd -e 63 | systemctl enable ssh 64 | cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF' 65 | country=US 66 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 67 | ap_scan=1 68 | 69 | update_config=1 70 | network={{ 71 | ssid="{ssid}" 72 | psk="{encrypted_wifi_password}" 73 | }} 74 | 75 | WPAEOF 76 | chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf 77 | rfkill unblock wifi 78 | for filename in /var/lib/systemd/rfkill/*:wlan ; do 79 | echo 0 > $filename 80 | done 81 | rm -f /boot/firstrun.sh 82 | sed -i 's| systemd.run.*||g' /boot/cmdline.txt 83 | exit 0 84 | ''' 85 | firstrun = firstrun.strip().splitlines() 86 | firstrun = "\n".join(firstrun) + "\n" 87 | 88 | 89 | def writefile(filename, content): 90 | """ 91 | writes the content into the file 92 | :param filename: the filename 93 | :param content: the content 94 | :return: 95 | """ 96 | outfile = io.open(path_expand(filename), 'w', newline='\n') 97 | outfile.write(content) 98 | outfile.flush() 99 | os.fsync(outfile) 100 | 101 | print (firstrun) 102 | 103 | 104 | if with_format: 105 | os.system(f"cms burn sdcard latest-lite --disk={disk}") 106 | 107 | cmdline_file = letter + ":/cmdline.txt" 108 | cmdline = Cmdline() 109 | cmdline.update(cmdline_file, version="lite") 110 | 111 | firstrun_file = letter + ":/firstrun.sh" 112 | writefile(firstrun_file, firstrun) 113 | 114 | #os.system(f"cp firstrun.sh /{letter}/") 115 | 116 | 117 | -------------------------------------------------------------------------------- /dev-scripts/ddd.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cloudmesh.common.util import path_expand 3 | from cloudmesh.common.util import readfile 4 | from cloudmesh.burn.raspberryos.cmdline import Cmdline 5 | from passlib.hash import sha256_crypt 6 | 7 | import sys 8 | import io 9 | from passlib.utils import pbkdf2 10 | import binascii, sys 11 | 12 | # USE 13 | # python ddd.py red SSID WIFIPASSWD PASSWD format 14 | # 15 | 16 | def psk_encrypt(ssid, password): 17 | value = pbkdf2.pbkdf2(str.encode(password), str.encode(ssid), 4096, 32) 18 | return binascii.hexlify(value).decode("utf-8") 19 | 20 | 21 | #def encrypt_user_passwd(passwd): 22 | # hash = sha512_crypt.encrypt(passwd) 23 | # return hash 24 | 25 | def encrypt_user_passwd(password): 26 | # repeatable salt if needed for testing 27 | # hash = sha256_crypt.using(salt='qY2oeR.YpL', rounds=5000).hash( 28 | # self.password) 29 | hash = sha256_crypt.using(rounds=5000).hash(password) 30 | return hash 31 | 32 | letter = "f" 33 | disk = "4" 34 | host = sys.argv[1] 35 | ssid = sys.argv[2] 36 | wifi_password = sys.argv[3] 37 | user_password = sys.argv[4] 38 | try: 39 | with_format = sys.argv[5] == "format" 40 | except: 41 | with_format = False 42 | 43 | encrypted_wifi_password = psk_encrypt(ssid, wifi_password) 44 | 45 | hash = encrypt_user_passwd(user_password) 46 | print(hash) 47 | 48 | # echo "$FIRSTUSER:"'{hash}' | chpasswd -e 49 | # fstring needs to be done carefully ....as there s { in it needs to be masked 50 | 51 | firstrun = \ 52 | f''' 53 | #!/bin/bash 54 | 55 | set +e 56 | 57 | CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \\t\\n\\r"` 58 | echo red >/etc/hostname 59 | sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t{host}/g" /etc/hosts 60 | FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 61 | FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 62 | echo "$FIRSTUSER:"'{hash}' | chpasswd -e 63 | systemctl enable ssh 64 | cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF' 65 | country=US 66 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 67 | ap_scan=1 68 | 69 | update_config=1 70 | network={{ 71 | ssid="{ssid}" 72 | psk="{encrypted_wifi_password}" 73 | }} 74 | 75 | WPAEOF 76 | chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf 77 | rfkill unblock wifi 78 | for filename in /var/lib/systemd/rfkill/*:wlan ; do 79 | echo 0 > $filename 80 | done 81 | rm -f /boot/firstrun.sh 82 | sed -i 's| systemd.run.*||g' /boot/cmdline.txt 83 | exit 0 84 | ''' 85 | 86 | sshkey = readfile(path_expand("~/.ssh/id_rsa.pub")) 87 | 88 | # BUG the script from anthony country=None was set and not country=US 89 | 90 | # SSH KEY read from ~/.ssh/id_rsa.pub 91 | #FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 92 | #FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 93 | #install -o "$FIRSTUSER" -m 700 -d "$FIRSTUSERHOME/.ssh" 94 | #install -o "$FIRSTUSER" -m 600 <(echo "{sshkey}") "$FIRSTUSERHOME/.ssh/authorized_keys" 95 | #cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig 96 | #echo 'PasswordAuthentication no' >>/etc/ssh/sshd_config 97 | 98 | 99 | firtsrun = ''' 100 | #!/bin/bash 101 | set +e 102 | CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \\t\\n\\r"` 103 | echo red >/etc/hostname 104 | sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t{host}/g" /etc/hosts 105 | echo 10.1.1.2 red01 >> /etc/hosts 106 | echo "interface eth0" >> /etc/dhcpcd.conf 107 | echo "static ip_address=10.1.1.1/24" >> /etc/dhcpcd.conf 108 | ======= 109 | 110 | ################CHANGES FOR DDD BEGIN######################################### 111 | 112 | 113 | country = "IN" 114 | locale = "en_US.UTF-8" 115 | timezone = "Asia/Kolkata" 116 | 117 | from cloudmesh.burn.raspberryos.runfirst import Runfirst 118 | from cloudmesh.common.systeminfo import os_is_windows 119 | 120 | runfirst = Runfirst() 121 | 122 | runfirst.set_hostname(host) 123 | 124 | if user_password: 125 | runfirst.set_password(password=user_password) 126 | 127 | runfirst.set_locale(timezone=timezone, locale=locale) 128 | 129 | if ssid: 130 | runfirst.set_wifi(ssid, wifi_password, country=country) 131 | 132 | runfirst.get(verbose=False) 133 | 134 | if os_is_windows(): 135 | runfirst.info() 136 | print(f"runscript: {letter}:/{Runfirst.SCRIPT_NAME}") 137 | filename = path_expand(f'~/.cloudmesh/cmburn/{Runfirst.SCRIPT_NAME}') 138 | runfirst.write(filename=filename) 139 | os.system(f"chmod a+x {filename}") 140 | 141 | writefile(filename=f'{letter}:/{Runfirst.SCRIPT_NAME}') 142 | os.system(f"chmod a+x {letter}:/{Runfirst.SCRIPT_NAME}") 143 | 144 | 145 | firstrun = \ 146 | f''' 147 | ''' 148 | 149 | firstrun = firstrun.strip().splitlines() 150 | firstrun = "\n".join(firstrun) + "\n" 151 | 152 | 153 | def writefile(filename, content): 154 | """ 155 | writes the content into the file 156 | :param filename: the filename 157 | :param content: the content 158 | :return: 159 | """ 160 | outfile = io.open(path_expand(filename), 'w', newline='\n') 161 | outfile.write(content) 162 | outfile.flush() 163 | os.fsync(outfile) 164 | 165 | print (firstrun) 166 | 167 | 168 | if with_format: 169 | os.system(f"cms burn sdcard latest-lite --disk={disk}") 170 | 171 | cmdline_file = letter + ":/cmdline.txt" 172 | cmdline = Cmdline() 173 | cmdline.update(cmdline_file, version="lite") 174 | 175 | firstrun_file = letter + ":/firstrun.sh" 176 | writefile(firstrun_file, firstrun) 177 | 178 | #os.system(f"cp firstrun.sh /{letter}/")''' 179 | 180 | 181 | -------------------------------------------------------------------------------- /dev-scripts/ddd1.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cloudmesh.common.util import path_expand 3 | from cloudmesh.common.util import readfile 4 | from cloudmesh.burn.raspberryos.cmdline import Cmdline 5 | from passlib.hash import sha256_crypt 6 | 7 | import sys 8 | import io 9 | from passlib.utils import pbkdf2 10 | import binascii, sys 11 | 12 | # USE 13 | # python ccc.py red SSID WIFIPASSWD PASSWD format 14 | # 15 | 16 | def psk_encrypt(ssid, password): 17 | value = pbkdf2.pbkdf2(str.encode(password), str.encode(ssid), 4096, 32) 18 | return binascii.hexlify(value).decode("utf-8") 19 | 20 | 21 | #def encrypt_user_passwd(passwd): 22 | # hash = sha512_crypt.encrypt(passwd) 23 | # return hash 24 | 25 | def encrypt_user_passwd(password): 26 | # repeatable salt if needed for testing 27 | # hash = sha256_crypt.using(salt='qY2oeR.YpL', rounds=5000).hash( 28 | # self.password) 29 | hash = sha256_crypt.using(rounds=5000).hash(password) 30 | return hash 31 | 32 | letter = "F" 33 | disk = "2" 34 | host = sys.argv[1] 35 | ssid = sys.argv[2] 36 | wifi_password = sys.argv[3] 37 | user_password = sys.argv[4] 38 | try: 39 | with_format = sys.argv[5] == "format" 40 | except: 41 | with_format = False 42 | 43 | encrypted_wifi_password = psk_encrypt(ssid, wifi_password) 44 | 45 | hash = encrypt_user_passwd(user_password) 46 | print(hash) 47 | 48 | # echo "$FIRSTUSER:"'{hash}' | chpasswd -e 49 | # fstring needs to be done carefully ....as there s { in it needs to be masked 50 | 51 | 52 | def writefile(filename, content): 53 | """ 54 | writes the content into the file 55 | :param filename: the filename 56 | :param content: the content 57 | :return: 58 | """ 59 | outfile = io.open(path_expand(filename), 'w', newline='\n') 60 | outfile.write(content) 61 | outfile.flush() 62 | os.fsync(outfile) 63 | 64 | 65 | ################CHANGES FOR DDD BEGIN######################################### 66 | 67 | 68 | country = "IN" 69 | locale = "en_US.UTF-8" 70 | timezone = "Asia/Kolkata" 71 | 72 | from cloudmesh.burn.raspberryos.runfirst import Runfirst 73 | from cloudmesh.common.systeminfo import os_is_windows 74 | 75 | runfirst = Runfirst() 76 | 77 | runfirst.set_hostname(host) 78 | 79 | if user_password: 80 | runfirst.set_password(password=user_password) 81 | 82 | runfirst.set_locale(timezone=timezone, locale=locale) 83 | 84 | if ssid: 85 | runfirst.set_wifi(ssid, wifi_password, country=country) 86 | 87 | runfirst.get(verbose=False) 88 | 89 | if os_is_windows(): 90 | runfirst.info() 91 | print(f"runscript: {letter}:/{Runfirst.SCRIPT_NAME}") 92 | filename = path_expand(f'~/.cloudmesh/cmburn/{Runfirst.SCRIPT_NAME}') 93 | runfirst.write(filename=filename) 94 | os.system(f"chmod a+x {filename}") 95 | 96 | writefile(filename=f'{letter}:/{Runfirst.SCRIPT_NAME}') 97 | os.system(f"chmod a+x {letter}:/{Runfirst.SCRIPT_NAME}") 98 | 99 | 100 | firstrun = \ 101 | f''' 102 | #!/bin/bash 103 | ccc.p 104 | set +e 105 | CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \\t\\n\\r"` 106 | echo red >/etc/hostname 107 | sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t{host}/g" /etc/hosts 108 | FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 109 | FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 110 | echo "$FIRSTUSER:"'{hash}' | chpasswd -e 111 | systemctl enable ssh 112 | cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF' 113 | country=US 114 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 115 | ap_scan=1 116 | update_config=1 117 | network={{ 118 | ssid="{ssid}" 119 | psk="{encrypted_wifi_password}" 120 | }} 121 | WPAEOF 122 | chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf 123 | rfkill unblock wifi 124 | for filename in /var/lib/systemd/rfkill/*:wlan ; do 125 | echo 0 > $filename 126 | done 127 | rm -f /boot/firstrun.sh 128 | sed -i 's| systemd.run.*||g' /boot/cmdline.txt 129 | exit 0 130 | ''' 131 | firstrun = firstrun.strip().splitlines() 132 | firstrun = "\n".join(firstrun) + "\n" 133 | 134 | 135 | print (firstrun) 136 | 137 | 138 | if with_format: 139 | os.system(f"cms burn sdcard latest-lite --disk={disk}") 140 | 141 | cmdline_file = letter + ":/cmdline.txt" 142 | cmdline = Cmdline() 143 | cmdline.update(cmdline_file, version="lite") 144 | 145 | firstrun_file = letter + ":/firstrun.sh" 146 | writefile(firstrun_file, firstrun) 147 | 148 | #os.system(f"cp firstrun.sh /{letter}/") 149 | 150 | -------------------------------------------------------------------------------- /dev-scripts/g.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cloudmesh.common.Shell import Shell 4 | from cloudmesh.common.util import writefile 5 | from cloudmesh.common.util import yn_choice 6 | import sys 7 | import string 8 | import win32api 9 | import win32wnet 10 | import win32netcon 11 | import subprocess 12 | import textwrap 13 | from cloudmesh.common.Tabulate import Printer 14 | from pathlib import Path 15 | 16 | class USB: 17 | @staticmethod 18 | def info(): 19 | print("Prints the table of information about devices on the usb info") 20 | 21 | 22 | ''' 23 | diskpart commands - 24 | https://www.windowscentral.com/how-mount-drive-windows-10#:~:text=Unmount%20drive%20with%20DiskPart&text=using%20these%20steps%3A-,Open%20Start.,Run%20as%20administrator**%20option.&text=Confirm%20the%20volume%20you%20want%20to%20unmount.&text=volume%20VOLUME%2DNUMBER-,In%20the%20command%2C%20replace%20VOLUME%2DNUMBER%20with%20the%20number%20of,volume)%20you%20want%20to%20mount. 25 | https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-xp/bb490893(v=technet.10)?redirectedfrom=MSDN 26 | ''' 27 | class SdCard: 28 | tmp = "tmp.txt" 29 | 30 | @staticmethod 31 | def clean(): 32 | # rm SDCard.tmp 33 | os.remove(SdCard.tmp) 34 | 35 | # Take a look at Gregor's SDCard init method 36 | @staticmethod 37 | def format_card(volume_number, disk_number): 38 | 39 | ''' 40 | # see https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/format 41 | if unmount: 42 | set_unmount = "/x" 43 | else: 44 | set_unmount = "" 45 | command = f"format {device} /fs:FAT32 /v:UNTITLED /q {set_unmount}".strip() 46 | print(command) 47 | ''' 48 | 49 | print(f"format :{volume_number}") 50 | script = textwrap.dedent(f""" 51 | select disk {disk_number} 52 | select volume {volume_number} 53 | format fs=fat32 quick 54 | """) 55 | 56 | try: 57 | a = SdCard.diskpart(script) 58 | except Exception as e: 59 | print(e) 60 | 61 | 62 | 63 | @staticmethod 64 | def mount(volume_number, volume_letter=None): 65 | #Figure out drive letter 66 | if volume_letter == None: 67 | volume_letter = SdCard.get_free_drive() 68 | a = SdCard.diskpart(f"select volume {volume_number}\nassign letter={volume_letter}") 69 | return volume_letter 70 | 71 | @staticmethod 72 | def unmount(volume_letter): 73 | b = SdCard.diskpart(f"remove letter={volume_letter}") 74 | # os.system(f"mountvol {device} /p") 75 | 76 | print(b) 77 | 78 | @staticmethod 79 | def write(volume_number, volume_letter): 80 | pass 81 | 82 | @staticmethod 83 | def sync(drive_letter): 84 | os.system(f"sync -r {drive_letter}") 85 | 86 | @staticmethod 87 | def eject(drive_letter): 88 | os.system(f"sync -e {drive_letter}") 89 | 90 | @staticmethod 91 | def diskpart(command): 92 | _diskpart = Path("C:/Windows/system32/diskpart.exe") 93 | writefile(SdCard.tmp, f"{command}\nexit") 94 | b = Shell.run(f"{_diskpart} /s {SdCard.tmp}") 95 | SdCard.clean() 96 | return b 97 | 98 | @staticmethod 99 | def info(): 100 | print("Disk info") 101 | b = SdCard.diskpart("list volume") 102 | 103 | lines = b.splitlines() 104 | result = [] 105 | for line in lines: 106 | if "Removable" in line and "Healthy" in line: 107 | result.append(line) 108 | 109 | info = [] 110 | for line in result: 111 | data = { 112 | 113 | "volume" : line[0:13].replace("Volume", "").strip(), 114 | "drive" : line[13:18].strip(), 115 | "label" : line[18:31].strip(), 116 | "fs" : line[31:38].strip(), 117 | "type" : line[38:50].strip(), 118 | "size" : line[50:59].strip(), 119 | "status" : line[59:70].strip(), 120 | 121 | } 122 | info.append(data) 123 | 124 | return info 125 | 126 | 127 | 128 | @staticmethod 129 | def get_free_drive(): 130 | """ 131 | 132 | """ 133 | drives = set(string.ascii_uppercase[2:]) 134 | for d in win32api.GetLogicalDriveStrings().split(':\\\x00'): 135 | drives.discard(d) 136 | # Discard persistent network drives, even if not connected. 137 | henum = win32wnet.WNetOpenEnum(win32netcon.RESOURCE_REMEMBERED, 138 | win32netcon.RESOURCETYPE_DISK, 0, None) 139 | while True: 140 | result = win32wnet.WNetEnumResource(henum) 141 | if not result: 142 | break 143 | for r in result: 144 | if len(r.lpLocalName) == 2 and r.lpLocalName[1] == ':': 145 | drives.discard(r.lpLocalName[0]) 146 | if drives: 147 | return sorted(drives)[-1] + ':' 148 | 149 | @staticmethod 150 | def guess_volume_number(): 151 | r = SdCard.info() 152 | print(r) 153 | for line in r: 154 | line = line.strip() 155 | if "*" not in line: 156 | line = line.replace("No Media", "NoMedia") 157 | line = line.replace("Disk ", "") 158 | line = ' '.join(line.split()) 159 | num, kind, size, unit, unused1, unused2 = line.split(" ") 160 | size = int(size) 161 | 162 | if unit == "GB" and (size > 7 and size < 128): 163 | return num 164 | raise ValueError("No SD card found") 165 | 166 | # SdCard.format_card(5, 3) 167 | info = SdCard.info() 168 | 169 | print(Printer.write(info, order=["volume", "drive", "fs", "label", "size"])) 170 | 171 | ''' 172 | DO NOT DELETE THIS COMMENT - WORKING CODE HERE. 173 | 174 | r = SdCard.info() 175 | print(r) 176 | volume_number = SdCard.guess_volume_number() 177 | 178 | if not yn_choice (f"Would you like to contine burning on disk {volume_number}"): 179 | sys.exit(0) 180 | 181 | volume_letter = SdCard.mount(volume_number) 182 | 183 | from glob import glob 184 | 185 | files = glob(f"{volume_letter}:") 186 | 187 | print(files) 188 | 189 | SdCard.unmount(volume_letter) 190 | ''' 191 | 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /dev-scripts/ggg.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cloudmesh.common.util import path_expand 3 | from cloudmesh.common.util import readfile 4 | from cloudmesh.burn.raspberryos.cmdline import Cmdline 5 | from passlib.hash import sha256_crypt 6 | 7 | import sys 8 | import io 9 | from passlib.utils import pbkdf2 10 | import binascii, sys 11 | 12 | # USE 13 | # python ddd.py red SSID WIFIPASSWD PASSWD format 14 | # 15 | 16 | def psk_encrypt(ssid, password): 17 | value = pbkdf2.pbkdf2(str.encode(password), str.encode(ssid), 4096, 32) 18 | return binascii.hexlify(value).decode("utf-8") 19 | 20 | 21 | #def encrypt_user_passwd(passwd): 22 | # hash = sha512_crypt.encrypt(passwd) 23 | # return hash 24 | 25 | def encrypt_user_passwd(password): 26 | # repeatable salt if needed for testing 27 | # hash = sha256_crypt.using(salt='qY2oeR.YpL', rounds=5000).hash( 28 | # self.password) 29 | hash = sha256_crypt.using(rounds=5000).hash(password) 30 | return hash 31 | 32 | letter = "f" 33 | disk = "4" 34 | host = sys.argv[1] 35 | ssid = sys.argv[2] 36 | wifi_password = sys.argv[3] 37 | user_password = sys.argv[4] 38 | try: 39 | with_format = sys.argv[5] == "format" 40 | except: 41 | with_format = False 42 | 43 | encrypted_wifi_password = psk_encrypt(ssid, wifi_password) 44 | #encrypted_wifi_password = wifi_password 45 | print(encrypted_wifi_password) 46 | 47 | hash = encrypt_user_passwd(user_password) 48 | print(hash) 49 | 50 | 51 | # echo "$FIRSTUSER:"'{hash}' | chpasswd -e 52 | # fstring needs to be done carefully ....as there s { in it needs to be masked 53 | 54 | 55 | firstrun_anthony = \ 56 | f''' 57 | sudo sed -i 's/#net\.ipv4\.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf 58 | sudo iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 59 | sudo iptables -A FORWARD -i wlan0 -o eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT 60 | sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE 61 | sh -c "iptables-save > /etc/iptables.ipv4.nat" 62 | sudo sed -i '$i iptables-restore < /etc/iptables.ipv4.nat' /etc/rc.local 63 | cat >/etc/wpa_supplicant/wpa_supplicant.conf < $filename 77 | done 78 | ''' 79 | 80 | firstrun_simple = \ 81 | f''' 82 | #!/bin/bash 83 | 84 | set +e 85 | 86 | CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \\t\\n\\r"` 87 | echo red >/etc/hostname 88 | sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t{host}/g" /etc/hosts 89 | FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 90 | FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 91 | echo "$FIRSTUSER:"'{hash}' | chpasswd -e 92 | systemctl enable ssh 93 | cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF' 94 | country=US 95 | ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev 96 | ap_scan=1 97 | 98 | update_config=1 99 | network={{ 100 | ssid="{ssid}" 101 | psk={encrypted_wifi_password} 102 | }} 103 | 104 | WPAEOF 105 | chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf 106 | rfkill unblock wifi 107 | for filename in /var/lib/systemd/rfkill/*:wlan ; do 108 | echo 0 > $filename 109 | done 110 | rm -f /boot/firstrun.sh 111 | sed -i 's| systemd.run.*||g' /boot/cmdline.txt 112 | exit 0 113 | ''' 114 | 115 | sshkey = readfile(path_expand("~/.ssh/id_rsa.pub")) 116 | 117 | # BUG the script from anthony country=None was set and not country=US 118 | 119 | # SSH KEY read from ~/.ssh/id_rsa.pub 120 | #FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 121 | #FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 122 | #install -o "$FIRSTUSER" -m 700 -d "$FIRSTUSERHOME/.ssh" 123 | #install -o "$FIRSTUSER" -m 600 <(echo "{sshkey}") "$FIRSTUSERHOME/.ssh/authorized_keys" 124 | #cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig 125 | #echo 'PasswordAuthentication no' >>/etc/ssh/sshd_config 126 | 127 | 128 | firstrun_cluster = f''' 129 | #!/bin/bash 130 | set +e 131 | CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \\t\\n\\r"` 132 | echo red >/etc/hostname 133 | sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\\t{host}/g" /etc/hosts 134 | echo 10.1.1.2 red01 >> /etc/hosts 135 | echo "interface eth0" >> /etc/dhcpcd.conf 136 | echo "static ip_address=10.1.1.1/24" >> /etc/dhcpcd.conf 137 | FIRSTUSER=`getent passwd 1000 | cut -d: -f1` 138 | FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6` 139 | echo "$FIRSTUSER:"'{hash}' | chpasswd -e 140 | systemctl enable ssh 141 | sudo sed -i 's/#net\.ipv4\.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf 142 | sudo iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 143 | sudo iptables -A FORWARD -i wlan0 -o eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT 144 | sudo iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE 145 | sh -c "iptables-save > /etc/iptables.ipv4.nat" 146 | sudo sed -i '$i iptables-restore < /etc/iptables.ipv4.nat' /etc/rc.local 147 | 148 | cat >/etc/wpa_supplicant/wpa_supplicant.conf < $filename 162 | done 163 | rm -f /etc/xdg/autostart/piwiz.desktop 164 | rm -f /etc/localtime 165 | echo "America/Indiana/Indianapolis" >/etc/timezone 166 | dpkg-reconfigure -f noninteractive tzdata 167 | 168 | # XKBLAYOUT="en_US.UTF-8" 169 | 170 | cat >/etc/default/keyboard <