diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index 6f1ebb0a..2a262b6b 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -12,10 +12,7 @@ name: Bandit on: - push: - branches: [ "master", "dev" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "master" ] jobs: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 06a4be05..ea67f38c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,10 +12,7 @@ name: "CodeQL" on: - push: - branches: [ "master", "dev" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "master" ] jobs: diff --git a/.github/workflows/license_scanner.yml b/.github/workflows/license_scanner.yml index f0b15cf1..2f368284 100644 --- a/.github/workflows/license_scanner.yml +++ b/.github/workflows/license_scanner.yml @@ -1,10 +1,7 @@ name: License Scanner on: - push: - branches: [ "master", "dev" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "master" ] jobs: @@ -22,7 +19,14 @@ jobs: - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y libgirepository1.0-dev + sudo apt-get install -y \ + libgirepository1.0-dev \ + libcairo2-dev \ + pkg-config \ + python3-dev \ + libffi-dev \ + gobject-introspection \ + libglib2.0-dev python -m pip install --upgrade pip pip install license_scanner pip install -e tower-lib diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 4bf648ec..ebd81caf 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -1,10 +1,7 @@ name: Pylint on: - push: - branches: [ "master", "dev" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "master" ] jobs: @@ -22,7 +19,14 @@ jobs: - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y libgirepository1.0-dev + sudo apt-get install -y \ + libgirepository1.0-dev \ + libcairo2-dev \ + pkg-config \ + python3-dev \ + libffi-dev \ + gobject-introspection \ + libglib2.0-dev python -m pip install --upgrade pip pip install pylint pip install pylint-sarif-unofficial diff --git a/.gitignore b/.gitignore index e74f1c69..b3a90d96 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +docs/src/whitepaper/toweros-whitepaper.pdf diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2bf6e2cd..3fd30b43 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -2,6 +2,7 @@ site_name: Documentation site_url: https://toweros.org repo_url: https://github.com/towercomputers/toweros/ repo_name: TowerOS +copyright: © Tower Computers 2023 docs_dir: src markdown_extensions: - attr_list diff --git a/docs/src/TowerOS Whitepaper.pdf b/docs/src/TowerOS Whitepaper.pdf index 2ad54518..597ee740 100644 Binary files a/docs/src/TowerOS Whitepaper.pdf and b/docs/src/TowerOS Whitepaper.pdf differ diff --git a/docs/src/diagram-usage.d2 b/docs/src/diagram-usage.d2 new file mode 100644 index 00000000..5b34c835 --- /dev/null +++ b/docs/src/diagram-usage.d2 @@ -0,0 +1,42 @@ +internet: Internet { + style.stroke: transparent + style.fill: transparent + icon: https://icons.terrastruct.com/essentials%2F140-internet.svg +} + +deskpi: DeskPi Super6C { + style.fill: transparent + + cm4-router: router { + icon: https://icons.terrastruct.com/infra%2F021-hardware.svg + } + cm4-web: web { + icon: https://icons.terrastruct.com/infra%2F021-hardware.svg + } + cm4-email: email { + icon: https://icons.terrastruct.com/infra%2F021-hardware.svg + } +} + +thinclient: Thin Client { + style.fill: transparent + window-web: Firefox { + icon: https://icons.terrastruct.com/essentials%2F006-window.svg + } + window-email: Thunderbird { + icon: https://icons.terrastruct.com/essentials%2F006-window.svg + } +} + + +deskpi.*.style.fill: transparent +deskpi.*.style.stroke: transparent + +thinclient.*.style.fill: transparent +thinclient.*.style.stroke: transparent + + + +deskpi.cm4-router <-> internet +deskpi.cm4-web -> thinclient.window-web: NX over SSH +deskpi.cm4-email -> thinclient.window-email: NX over SSH diff --git a/docs/src/guides.md b/docs/src/guides.md index a30cb34d..563129ae 100644 --- a/docs/src/guides.md +++ b/docs/src/guides.md @@ -60,7 +60,8 @@ Before installing a package with `pip`, check that there is no `apk` package ins 1. Install `pip` package in offline host -To install a package with `pip` you must create a virtual environment. Please refer to [the official documentation](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/). +To install a package with `pip` you must create a virtual environment. Please refer to **[the official documentation](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/)**. +Make sure to install your environment in the `/home` folder if you want it to be preserved during an upgrade. [thinclient]$ ssh office [office]$ mkdir myproject diff --git a/docs/src/whitepaper/drafts/2025-12-27_toweros-whitepaper.pdf b/docs/src/whitepaper/drafts/2025-12-27_toweros-whitepaper.pdf new file mode 100644 index 00000000..597ee740 Binary files /dev/null and b/docs/src/whitepaper/drafts/2025-12-27_toweros-whitepaper.pdf differ diff --git a/docs/src/whitepaper/main.pdf b/docs/src/whitepaper/main.pdf new file mode 100644 index 00000000..4626eff9 Binary files /dev/null and b/docs/src/whitepaper/main.pdf differ diff --git a/docs/src/whitepaper/toweros-whitepaper.typ b/docs/src/whitepaper/main.typ similarity index 55% rename from docs/src/whitepaper/toweros-whitepaper.typ rename to docs/src/whitepaper/main.typ index 8b0afc5e..e69b5f1b 100644 --- a/docs/src/whitepaper/toweros-whitepaper.typ +++ b/docs/src/whitepaper/main.typ @@ -1,4 +1,4 @@ -#import "template.typ": * +#import "style/ieee.typ": * #show: ieee.with( title: "TowerOS: An Operating System for Network-Boundary Converged Multi-Level Secure Computing", @@ -9,16 +9,15 @@ url: "https://github.com/towercomputers/toweros" ), ), + abstract: [We describe TowerOS, a system architecture for converged multi-level secure (MLS) computing in which isolation between security domains is enforced at a network boundary rather than within a single shared-kernel or hypervisor-based platform. Each security domain runs on an independent headless host, while a thin client provides a unified user interface by compositing remote application displays over standard, widely-deployed cryptographic protocols. This design aims to reduce the cross-domain trusted computing base relative to software-boundary approaches while retaining the usability benefits of a single, integrated desktop.], ) -#set text(font: "Palatino") #set quote(block: true) -#set enum(spacing: 500%) +#set enum(spacing: 1em) #show quote: it => [ #set block(width: 100%) #emph[#it] ] -#set quote(block: true) = Background A converged multi-level secure (MLS) computing system is one that allows the user to operate across distinct security domains through a single user interface (UI). Traditional MLS systems rely on hardware-level isolation using a keyboard-video-mouse (KVM) switch and no UI compositing@soffer or more recently with software-level isolation and software-based UI compositing (e.g. using a hypervisor).@issa While hardware-level isolation is theoretically much more secure than software-level isolation, the overall usability of any MLS system without user-interface compositing is necessarily poor in comparison, because there is no single, unified interface provided for the user. @@ -29,28 +28,21 @@ Recently, a system for hardware-level isolation with hardware-based UI compositi = Architecture -TowerOS implements a new, hybrid design which performs _software-based user-interface compositing_ with _hardware-level isolation_ using standard network interfaces. TowerOS relegates each security domain to an independent headless computer, each with its own application state and security policies. These *Hosts* are networked together over a LAN and accessible by the user through a *Thin Client* device that is connected to the same network. The applications running on the various hosts are composited within a single user interface running on the thin client using a combination of multi-function network protocols (such as SSH) and desktop-sharing software (such as VNC over SSL). - -Instead of having to trust an operating system to be able properly to isolate different security domains all running on shared hardware, our design relies on cryptographically secure networking protocols to connect multiple independent computers together to form a single, virtual device that from the user's perspective functions very much like a normal desktop computer. Instead of running multiple virtual machines on a single computer (whether to save costs or to isolate different security domains at the level of a hypervisor) we instead merge together multiple computers into a single virtual machine, where the actual hardware that any given application runs on (for security, or, for that matter, for performance) is abstracted away. This provides for the best of both words: the security guarantees of hardware isolation plus the usability and flexibility of interfaces implemented in software. +TowerOS implements a new, hybrid design, which performs _software-based user-interface compositing_ with _hardware-level isolation_ using standard network interfaces. Each security domain runs on an independent headless computer (a *host*), with its own application state and security policies. Hosts are connected over a LAN and accessed by the user through a *thin client* connected to the same network. Applications running on different hosts are presented within a single user interface on the thin client using a combination of multi-function network protocols (such as SSH) and remote-display software (such as VNC over an authenticated and encrypted transport). -Such a system may be built exclusively with commercial off-the-shelf (COTS) hardware, and its trusted computing base (TCB) of the system is limited to the codebase for the networking protocols (SSH, etc.), which may be both widely used and easily audited. For example, each host would run whichever user applications are allowed within the security domain associated with the device in question. So one host might be running an e-mail client, another a word processor, another a password manager, and another a web browser. One host might be left stateless and reserved for hotloading with fresh copies of an operating system. The user would be able to access each of these applications from the laptop thin client using SSH and VNC, with application windows composited into a single graphical user interface (GUI) using the desktop compositor. Clipboard management could be performed on the laptop using the thin client, and file transfers could be handled easily with `scp` or with a local file browser and `sshfs`. +Instead of having to trust an operating system to be able properly to isolate different security domains all running on shared hardware, our design relies on cryptographically secure networking protocols to connect multiple independent computers together to form a single, virtual device that from the user's perspective functions very much like a normal desktop computer. Instead of running multiple virtual machines on a single computer (whether to save costs or to isolate different security domains at the level of a hypervisor) we instead merge together multiple computers into a single virtual machine, where the actual hardware that any given application runs on (for security, or, for that matter, for performance) is abstracted away. This provides for the best of both worlds: the security guarantees of hardware isolation plus the usability and flexibility of interfaces implemented in software. +Such a system may be built exclusively with commercial off-the-shelf (COTS) hardware. In contrast to software-boundary approaches, the core isolation boundary is not an in-kernel or in-hypervisor mechanism, but a network interface between physically independent machines. The cross-domain TCB is therefore dominated by the thin client's networking and remote-display stacks, plus the cryptographic protocol implementations used to connect to each host. For example, each host would run whichever user applications are allowed within the security domain associated with the device in question. So one host might be running an e-mail client, another a word processor, another a password manager, and another a web browser. One host might be left stateless and reserved for hotloading with fresh copies of an operating system. The user would access these applications from the thin client using SSH and VNC, with application windows presented within a single graphical user interface (GUI). Clipboard management can be performed on the thin client, and file transfers can be handled with `scp` or with a local file browser and `sshfs`. = Threat Model -The security properties of this design compare very favorably to those of software-boundary multi-level secure systems. First and foremost, such solutions rely on a large trusted computing base, including not only the (very complex) hypervisor, but also much of the underlying hardware (also very complex!) The network boundary is an ideal security boundary because it was historically designed explicitly for the interconnection of independent devices, often with different security policies. Both the hardware interface and the software compositing layer -are small and well understood. - -The only data being _pushed_ to the thin client are pixels, clipboard data and audio streams from the hosts (and data are never communicated directly from host to host.) As a consequence, so long as the user of the thin client doesn't explicitly _pull_ malware onto the device, say with SSH, the risk of compromising the thin client (and by extension, the other hosts) is practically-speaking limited to the risk of critical input validation errors in the screen-sharing software itself or at the level of the network drivers. That is, even if the UI compositor on the thin-client machine does not enforce any security boundaries between application windows, the primary attack surface is limited to the only application running _in_ those windows, e.g. VNC. +The security properties of this design compare favorably to those of software-boundary multi-level secure systems. Such solutions rely on a large trusted computing base, including not only a complex hypervisor, but also large driver stacks and substantial portions of the underlying hardware. By contrast, TowerOS moves the isolation boundary to the network interface between independent machines and relies on widely deployed, cryptographically protected network protocols for access to each domain. The cross-domain attack surface is therefore concentrated in the thin client's networking and remote-display stacks rather than in a hypervisor and its hardware-facing substrate. -/* TODO -Side-Channel Attacks -- Traffic Analysis -- Electromagnetic and Acoustic -*/ +The primary untrusted inputs that a compromised host can push to the thin client are pixels, clipboard data, and audio streams. Direct host-to-host communication is forbidden (for example, by physical network topology, managed-switch isolation features, and OS-level firewall rules). As a consequence, so long as the user of the thin client does not explicitly pull untrusted executables onto the thin client, the main risk of compromising the thin client (and by extension, other domains) is limited to exploitable bugs in the thin client's protocol implementations, parsing, and rendering code (for example in remote-display software and the network stack). +Crucially, TowerOS does not rely on compositor-level isolation between windows on the thin client as an enforcement boundary between security domains. Applications execute on hosts; the thin client primarily renders remote-display content and forwards user input over authenticated and encrypted channels. Even if the thin-client compositor treats all windows as peers, cross-domain compromise still requires an exploit in the thin client's networking / remote-display / clipboard / audio handling code, rather than a bypass of a compositor-enforced sandbox between local applications. = Comparison with Qubes OS -The state-of-the-art in secure computing systems@snowden is #link("http://qubes-os.org")[Qubes OS] is an open-source converged multi-level secure operating system that uses hardware virtualization (with Xen) to isolate security domains. As the former lead developer of GrapheneOS put it: +The state-of-the-art in secure computing systems@snowden is #link("http://qubes-os.org")[Qubes OS], an open-source converged multi-level secure operating system that uses hardware virtualization (with Xen) to isolate security domains. As the former lead developer of GrapheneOS put it: #quote(attribution: "D. Micay")[You can think of QubesOS as a way of approximating having 20 laptops with their own purposes, but all on 1 laptop. The security of each compartment still matters, and beyond isolating some drivers it doesn't do much to address that, but it does successfully approximate air gapped machines to a large extent. It's still significantly more secure to have separate machines but it's very impractical / unrealistic especially at that scale. There is no better option for approximating the security of using separate computers for different sets of tasks / identities.@graphene] @@ -59,16 +51,19 @@ With TowerOS, we hope to address this deficiency in the software ecosystem. In t == Advantages #enum( enum.item(1)[Most importantly, Qubes OS relies heavily on the security guarantees of Xen, which is large, complicated, and has a history of serious security vulnerabilities.@deraadt -#v(-15pt) + #quote(attribution: "J. Rutkowska")[“In recent years, as more and more top notch researchers have begun scrutinizing Xen, a number of #link("https://xenbits.xen.org/xsa/")[security bugs] have been discovered. While #link("https://www.qubes-os.org/security/xsa/")[many] of them did not affect the security of Qubes OS, there were still too many that did.”@rutkowska]#v(5pt)], enum.item(2)[Qubes OS relies on the security properties of the hardware it runs on. -#v(-15pt) + #quote(attribution: "J. Rutkowska")[“Other problems arise from the underlying architecture of the x86 platform, where various inter-VM side- and covert-channels are made possible thanks to the aggressively optimized multi-core CPU architecture, most spectacularly demonstrated by the recently published #link("https://meltdownattack.com")[Meltdown and Spectre attacks]. Fundamental problems in other areas of the underlying hardware have also been discovered, such as the #link("https://googleprojectzero.blogspot.com/2015/03/exploiting-dram-rowhammer-bug-to-gain.html")[Row Hammer Attack].”@rutkowska]#v(5pt)], -enum.item(3)[The complexity inherent in the design of Qubes OS makes the operating system difficult both to maintain and to use. Accordingly, Qubes OS development has slowed significantly in recent years: as of December 2022, the last release (v4.1.x, in February 2022) came almost four years after the previous one (v4.0.x in March 2018).@download], + // TODO + // https://comsec.ethz.ch/research/dram/zenhammer/ + +enum.item(3)[The complexity inherent in the design of Qubes OS makes the operating system difficult both to maintain and to use. Accordingly, Qubes OS releases have historically been relatively infrequent, with multi-year gaps between major versions.@download], -enum.item(4)[Qubes OS has support only for extremely few hardware configurations. As of December 2022, are only three laptops that are known to be fully compatible with Qubes OS.@certified With only moderate effort, TowerOS may be hybridized with any modern operating system so long as that operating system supports the standard network interfaces required for SSH, etc. This flexibility can enable the system to run a wide variety of software and hardware.] +enum.item(4)[Qubes OS has support only for comparatively few hardware configurations, with an explicit certified hardware list.@certified With only moderate effort, TowerOS may be hybridized with any modern operating system so long as that operating system supports the standard network interfaces required for SSH, etc. This flexibility can enable the system to run a wide variety of software and hardware.] ) == Disadvantages @@ -84,4 +79,11 @@ enum.item(3)[In some cases, the security domain isolation within Qubes OS may be = Conclusion Using “physically separate qubes” was proposed in the Qubes OS blog post cited above (in a hybrid design similar to what is being described here);@rutkowska but the suggested architecture would leave hardware-boundary isolation as a second-class citizen and, by continuing to rely on a derivative of today's Qubes OS, preserve all of the hardware-support, maintainability and usability issues that the OS suffers from today. An operating system desired specifically for pure network-boundary converged multi-level secure computing, as described in this document, is simultaneously much simpler, more secure and more user-friendly than Qubes OS. Indeed, this design addresses all of the major problems with QubesOS completely, and with a qualitatively smaller codebase. +/* TODO +Future Work +- Side-Channel Attacks + - Traffic Analysis + - Electromagnetic and Acoustic +*/ + #bibliography("refs.yml") diff --git a/docs/src/whitepaper/style/ieee.typ b/docs/src/whitepaper/style/ieee.typ new file mode 120000 index 00000000..9835767b --- /dev/null +++ b/docs/src/whitepaper/style/ieee.typ @@ -0,0 +1 @@ +/Users/adam/Documents/work/writing/auxiliary/templates/ieee.typ \ No newline at end of file diff --git a/docs/src/whitepaper/template.typ b/docs/src/whitepaper/template.typ deleted file mode 100644 index 317fd3ef..00000000 --- a/docs/src/whitepaper/template.typ +++ /dev/null @@ -1,164 +0,0 @@ -// This function gets your whole document as its `body` and formats -// it as an article in the style of the IEEE. -#let ieee( - // The paper's title. - title: "Paper Title", - - // An array of authors. For each author you can specify a name, - // department, organization, location, and email. Everything but - // but the name is optional. - authors: (), - - // The paper's abstract. Can be omitted if you don't have one. - abstract: none, - - // A list of index terms to display after the abstract. - index-terms: (), - - // The article's paper size. Also affects the margins. - paper-size: "us-letter", - - // The path to a bibliography file if you want to cite some external - // works. - bibliography-file: none, - - // The paper's content. - body -) = { - // Set document metadata. - set document(title: title, author: authors.map(author => author.name)) - - // Set the body font. - set text(font: "STIX Two Text", size: 10pt) - - // Configure the page. - set page( - paper: paper-size, - // The margins depend on the paper size. - margin: if paper-size == "a4" { - (x: 41.5pt, top: 80.51pt, bottom: 89.51pt) - } else { - ( - x: (50pt / 216mm) * 100%, - top: (55pt / 279mm) * 100%, - bottom: (64pt / 279mm) * 100%, - ) - } - ) - - // Configure equation numbering and spacing. - set math.equation(numbering: "(1)") - show math.equation: set block(spacing: 0.65em) - - // Configure lists. - set enum(indent: 10pt, body-indent: 9pt) - set list(indent: 10pt, body-indent: 9pt) - - // Configure headings. - set heading(numbering: "I.A.1.") - show heading: it => locate(loc => { - // Find out the final number of the heading counter. - let levels = counter(heading).at(loc) - let deepest = if levels != () { - levels.last() - } else { - 1 - } - - set text(10pt, weight: 400) - if it.level == 1 [ - // First-level headings are centered smallcaps. - // We don't want to number of the acknowledgment section. - #let is-ack = it.body in ([Acknowledgment], [Acknowledgement]) - #set align(center) - #set text(if is-ack { 10pt } else { 12pt }) - #show: smallcaps - #v(20pt, weak: true) - #if it.numbering != none and not is-ack { - numbering("I.", deepest) - h(7pt, weak: true) - } - #it.body - #v(13.75pt, weak: true) - ] else if it.level == 2 [ - // Second-level headings are run-ins. - #set par(first-line-indent: 0pt) - #set text(style: "italic") - #v(10pt, weak: true) - #if it.numbering != none { - numbering("A.", deepest) - h(7pt, weak: true) - } - #it.body - #v(10pt, weak: true) - ] else [ - // Third level headings are run-ins too, but different. - #if it.level == 3 { - numbering("1)", deepest) - [ ] - } - _#(it.body):_ - ] - }) - - // Display the paper's title. - v(3pt, weak: true) - align(center, text(18pt, title)) - v(8.35mm, weak: true) - - // Display the authors list. - for i in range(calc.ceil(authors.len() / 3)) { - let end = calc.min((i + 1) * 3, authors.len()) - let is-last = authors.len() == end - let slice = authors.slice(i * 3, end) - grid( - columns: slice.len() * (1fr,), - gutter: 12pt, - ..slice.map(author => align(center, { - text(12pt, author.name) - if "department" in author [ - \ #emph(author.department) - ] - if "organization" in author [ - \ #emph(author.organization) - ] - if "location" in author [ - \ #author.location - ] - if "email" in author [ - \ #link("mailto:" + author.email) - ] - })) - ) - - if not is-last { - v(16pt, weak: true) - } - } - v(40pt, weak: true) - - // Start two column mode and configure paragraph properties. - show: columns.with(2, gutter: 12pt) - set par(justify: true, first-line-indent: 1em) - show par: set block(spacing: 0.65em) - - // Display abstract and index terms. - if abstract != none [ - #set text(weight: 700) - #h(1em) _Abstract_---#abstract - - #if index-terms != () [ - #h(1em)_Index terms_---#index-terms.join(", ") - ] - #v(2pt) - ] - - // Display the paper's contents. - body - - // Display bibliography. - if bibliography-file != none { - show bibliography: set text(8pt) - bibliography(bibliography-file, title: text(10pt)[References], style: "ieee") - } -} diff --git a/tower-apks/toweros-host/overlay/var/towercomputers/installer/install-host.sh b/tower-apks/toweros-host/overlay/var/towercomputers/installer/install-host.sh index b4bb61fd..1c8bbe66 100755 --- a/tower-apks/toweros-host/overlay/var/towercomputers/installer/install-host.sh +++ b/tower-apks/toweros-host/overlay/var/towercomputers/installer/install-host.sh @@ -9,6 +9,7 @@ update_passord() { sed -i "s/^$1:[^:]*:/$ESCAPED_REPLACE/g" /etc/shadow } + check_and_copy_key_from_boot_disk() { if ! [ -f "$BOOT_MEDIA/crypto_keyfile.bin" ]; then echo "Key file not found in boot partition" @@ -23,6 +24,7 @@ check_and_copy_key_from_boot_disk() { chmod 0400 /crypto_keyfile.bin } + create_lvm_partitions() { # zeroing root device dd if=/dev/zero of=$LVM_DISK bs=512 count=1 conv=notrunc @@ -48,6 +50,7 @@ create_lvm_partitions() { HOME_PARTITION="/dev/vg0/home" } + activate_lvm_disk() { # initialize the LUKS partition cryptsetup luksOpen $LVM_DISK lvmcrypt --key-file=/crypto_keyfile.bin @@ -57,6 +60,7 @@ activate_lvm_disk() { ROOT_PARTITION="/dev/vg0/root" } + prepare_root_partition() { if [ "$INSTALLATION_TYPE" == "install" ]; then create_lvm_partitions @@ -81,6 +85,7 @@ prepare_root_partition() { cp /crypto_keyfile.bin /mnt/crypto_keyfile.bin } + prepare_home_directory() { # create first user adduser -D "$USERNAME" "$USERNAME" @@ -107,6 +112,7 @@ EOF chown -R "$USERNAME:$USERNAME" "/mnt/home/$USERNAME" } + update_live_system() { # TODO: set locale setup-hostname -n $HOSTNAME @@ -197,6 +203,7 @@ EOF chmod 600 /etc/ssh/ssh_host_* } + clone_live_system_to_disk() { # install base system ovlfiles=/tmp/ovlfiles @@ -228,9 +235,8 @@ clone_live_system_to_disk() { # install packages local apkflags="--initdb --quiet --progress --update-cache --clean-protected" - local pkgs="$(grep -h -v -w sfdisk /mnt/etc/apk/world 2>/dev/null)" local repoflags="--repository $BOOT_MEDIA/apks" - apk add --root /mnt $apkflags --overlay-from-stdin --force-overwrite $repoflags $pkgs <$ovlfiles + apk add --root /mnt $apkflags --overlay-from-stdin --force-overwrite $repoflags $DEFAULT_PACKAGES <$ovlfiles # clean chroot umount /mnt/proc @@ -254,21 +260,19 @@ clone_live_system_to_disk() { cmdline="modules=$modules $kernel_opts" echo "$cmdline" > $BOOT_MEDIA/cmdline.txt - # Get branch from buildhost.py - # configure apk repositories if host is online - if [ "$HOSTNAME" == "router" ] || [ "$ONLINE" == "true" ]; then - mkdir -p /mnt/etc/apk - cat < /mnt/etc/apk/repositories + # configure apk repositories + mkdir -p /mnt/etc/apk + cat < /mnt/etc/apk/repositories http://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/main http://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/community #http://dl-cdn.alpinelinux.org/alpine/edge/testing EOF - fi # migrate from sudo to doas ln -s /usr/bin/doas /mnt/usr/bin/sudo || true } + clean_and_reboot() { # disable auto installation on boot mv /mnt/etc/local.d/install-host.start /mnt/etc/local.d/install.bak || true @@ -282,11 +286,12 @@ clean_and_reboot() { reboot } + init_configuration() { # tower.env MUST contains the following variables: # HOSTNAME, USERNAME, PUBLIC_KEY, PASSWORD_HASH, KEYBOARD_LAYOUT, KEYBOARD_VARIANT, # TIMEZONE, LANG, ONLINE, WLAN_SSID, WLAN_SHARED_KEY, THIN_CLIENT_IP, TOWER_NETWORK, - # STATIC_HOST_IP, ROUTER_IP, INSTALLATION_TYPE, COLOR, ALPINE_BRANCH + # STATIC_HOST_IP, ROUTER_IP, INSTALLATION_TYPE, COLOR, ALPINE_BRANCH, DEFAULT_PACKAGES if [ -f /media/usb/tower.env ]; then # boot on usb source /media/usb/tower.env diff --git a/tower-apks/toweros-host/overlay/var/towercomputers/scripts/host-to-buider.sh b/tower-apks/toweros-host/overlay/var/towercomputers/scripts/host-to-buider.sh new file mode 100644 index 00000000..87832ec5 --- /dev/null +++ b/tower-apks/toweros-host/overlay/var/towercomputers/scripts/host-to-buider.sh @@ -0,0 +1,19 @@ +sudo apk add alpine-sdk xz rsync perl-utils musl-locales \ + py3-pip py3-requests py3-rich cairo cairo-dev python3-dev \ + gobject-introspection gobject-introspection-dev \ + xsetroot losetup squashfs-tools xorriso pigz mtools + +sudo addgroup tower abuild +abuild-keygen -a -i + +cd tower-lib +sudo pip install -e . --break-system-packages +cd ../tower-cli +sudo pip install -e . --break-system-packages --no-deps + +# sudo sfdisk --delete /dev/sdb +# sudo parted --script /dev/sdb mklabel msdos +# sudo parted --script /dev/sdb mkpart primary fat32 0% 100% +# sudo mkdosfs -n bootfs -F 32 -s 4 -v /dev/sdb1 +# sudo mount /dev/sdb1 MNT +# sudo tar -xpf toweros-thinclient-0.7.0-aarch64.tar.gz -C MNT/ \ No newline at end of file diff --git a/tower-apks/toweros-thinclient-builds/APKBUILD b/tower-apks/toweros-thinclient-builds/APKBUILD new file mode 100644 index 00000000..178dda31 --- /dev/null +++ b/tower-apks/toweros-thinclient-builds/APKBUILD @@ -0,0 +1,28 @@ +CURRENT_DIR=$(pwd) +ROOT_DIR="$CURRENT_DIR/../.." + +pkgname=toweros-thinclient-builds +pkgver=$(cat $ROOT_DIR/tower-lib/towerlib/__about__.py | awk '{print $NF}' | sed 's/"//g') +pkgrel=0 +pkgdesc="TowerOS-ThinClient Builds" +url="https://toweros.org/" +arch="x86_64" +license="Apache-2.0" + + +build() { + # build toweros-host image + cd $ROOT_DIR/tower-build-cli + ./tower-build host --build-dir $srcdir + cd $CURRENT_DIR +} + +check() { + return 0 +} + +package() { + # install host images + mkdir -p $pkgdir/var/towercomputers/builds + cp $srcdir/*.xz $pkgdir/var/towercomputers/builds/ +} \ No newline at end of file diff --git a/tower-apks/toweros-thinclient/APKBUILD b/tower-apks/toweros-thinclient/APKBUILD index d92db514..b35b31aa 100644 --- a/tower-apks/toweros-thinclient/APKBUILD +++ b/tower-apks/toweros-thinclient/APKBUILD @@ -6,7 +6,7 @@ pkgver=$(cat $ROOT_DIR/tower-lib/towerlib/__about__.py | awk '{print $NF}' | se pkgrel=0 pkgdesc="TowerOS-ThinClient" url="https://toweros.org/" -arch="x86_64" +arch="x86_64 aarch64" license="Apache-2.0" depends=$(cat $CURRENT_DIR/world | tr '\n' ' ') makedepends="py3-gpep517 python3-dev py3-hatchling py3-wheel oxygen-icons5" @@ -43,12 +43,12 @@ build() { --output $srcdir/tower.1 gzip $srcdir/tower.1 # generate iptables rules + sudo cp /etc/iptables/rules-save /etc/iptables/rules-save.bak sudo sh $CURRENT_DIR/overlay/var/towercomputers/installer/configure-firewall.sh sudo iptables-save > $srcdir/rules-save - # build toweros-host image - cd $ROOT_DIR/tower-build-cli - ./tower-build host --build-dir $srcdir - cd $CURRENT_DIR + sudo mv /etc/iptables/rules-save.bak /etc/iptables/rules-save + sudo cat /etc/iptables/rules-save | sudo iptables-restore + sudo rc-service iptables restart # copy oxygen theme icons mkdir -p $srcdir/icons cp -r /usr/share/icons/oxygen/base/48x48/* $srcdir/icons/ @@ -82,9 +82,9 @@ package() { chmod +x $pkgdir/etc/init.d/wl-copy-watch chmod +x $pkgdir/etc/init.d/wl-copy-tunneler chmod +x $pkgdir/etc/profile.d/tower-env.sh - # install host images - mkdir -p $pkgdir/var/towercomputers/builds - cp $srcdir/*.xz $pkgdir/var/towercomputers/builds/ + # install sysctl.conf + local current_arch="$(arch)" + cp $pkgdir/etc/sysctl.d/local.conf.$current_arch $pkgdir/etc/sysctl.d/local.conf # install docs cp -r $srcdir/docs $pkgdir/var/towercomputers/ mkdir -p $pkgdir/usr/share/man/man1 @@ -103,8 +103,8 @@ package() { rc_add killprocs shutdown rc_add savecache shutdown # add services needed by toweros - rc_add lvm default - rc_add dmcrypt default + rc_add lvm boot + rc_add dmcrypt boot rc_add iptables default rc_add dbus default rc_add local default diff --git a/tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf.aarch64 b/tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf.aarch64 new file mode 100644 index 00000000..963116fc --- /dev/null +++ b/tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf.aarch64 @@ -0,0 +1,64 @@ +# source: https://madaidans-insecurities.github.io/guides/linux-hardening.html#kernel + +### Kernel ### + +# mitigate kernel pointer leaks +kernel.kptr_restrict=2 +# restricts the kernel log to the CAP_SYSLOG capability +kernel.dmesg_restrict=1 +# prevents screen information leaks during boot +kernel.printk=3 3 3 3 +# restrict eBPF to the CAP_BPF capability (CAP_SYS_ADMIN on kernel versions prior to 5.8) +kernel.unprivileged_bpf_disabled=1 +# This restricts loading TTY line disciplines to the CAP_SYS_MODULE capability +dev.tty.ldisc_autoload=0 +# disable SysRq +kernel.sysrq=0 +# restricts all usage of performance events to the CAP_PERFMON capability (CAP_SYS_ADMIN on kernel versions prior to 5.8) +kernel.perf_event_paranoid=3 + +### Network ### + +# This helps protect against SYN flood attacks +net.ipv4.tcp_syncookies=1 +# This protects against time-wait assassination +net.ipv4.tcp_rfc1337=1 +# protects against IP spoofing +net.ipv4.conf.all.rp_filter=1 +net.ipv4.conf.default.rp_filter=1 +# disable ICMP redirect acceptance and sending to prevent man-in-the-middle attacks +net.ipv4.conf.all.accept_redirects=0 +net.ipv4.conf.default.accept_redirects=0 +net.ipv4.conf.all.secure_redirects=0 +net.ipv4.conf.default.secure_redirects=0 +net.ipv6.conf.all.accept_redirects=0 +net.ipv6.conf.default.accept_redirects=0 +net.ipv4.conf.all.send_redirects=0 +net.ipv4.conf.default.send_redirects=0 +# This setting makes your system ignore all ICMP requests to avoid Smurf attacks +net.ipv4.icmp_echo_ignore_all=1 +# disable source routing +net.ipv4.conf.all.accept_source_route=0 +net.ipv4.conf.default.accept_source_route=0 +net.ipv6.conf.all.accept_source_route=0 +net.ipv6.conf.default.accept_source_route=0 +# disable IPv6 router advertisements +net.ipv6.conf.all.accept_ra=0 +net.ipv6.conf.default.accept_ra=0 +# disables TCP SACK +net.ipv4.tcp_sack=0 +net.ipv4.tcp_dsack=0 +net.ipv4.tcp_fack=0 + +### User Space ### + +# Increase the bits of entropy used for mmap ASLR, improving its effectiveness. +#vm.mmap_rnd_bits=32 +#vm.mmap_rnd_compat_bits=16 +# only permits symlinks to be followed when outside of a world-writable sticky directory, when the owner of the symlink and follower match or when the directory owner matches the symlink's owner +fs.protected_symlinks=1 +# prevents hardlinks from being created by users that do not have read/write access to the source file +fs.protected_hardlinks=1 +# These prevent creating files in potentially attacker-controlled environments +fs.protected_fifos=2 +fs.protected_regular=2 \ No newline at end of file diff --git a/tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf b/tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf.x86_64 similarity index 100% rename from tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf rename to tower-apks/toweros-thinclient/overlay/etc/sysctl.d/local.conf.x86_64 diff --git a/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/askconfiguration.py b/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/askconfiguration.py index f5563f4a..1d93d130 100755 --- a/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/askconfiguration.py +++ b/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/askconfiguration.py @@ -16,6 +16,7 @@ from towerlib import provision from towerlib.utils.decorators import join_list +from towerlib.config import THINCLIENT_DEFAULT_PACKAGES, THINCLIENT_ALPINE_BRANCH LOCALE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'locale.json') with open(LOCALE_FILE, "r", encoding="UTF-8") as file_pointer: @@ -34,27 +35,28 @@ def run_cmd(cmd, to_json=False): return out -def get_mountpoints(): - all_devices = run_cmd(['lsblk', '-J'], to_json=True) - mountpoints = {} - for device in all_devices['blockdevices']: - if device['type'] == 'disk': - mountpoints[f"/dev/{device['name']}"] = device['mountpoints'][0] - return mountpoints - - def disk_list(exclude=None): - all_disks = run_cmd(['lsscsi']).split("\n") - mountpoints = get_mountpoints() + all_disks = run_cmd(['lsblk', '-o', 'MODEL,VENDOR,SIZE,TYPE,MOUNTPOINTS,PATH', '-J', '-d'], to_json=True) disks = [] - for disk in all_disks: - path = disk[disk.index('/dev/'):].split(" ")[0].strip() - if exclude and path == exclude: + for device in all_disks['blockdevices']: + if device['type'] != 'disk': continue - if path not in mountpoints: + if exclude and device['path'] == exclude: continue - if mountpoints[path] is not None: + if device['mountpoints'][0] is not None: continue + if device['vendor'] is None: + if device['path'].startswith('/dev/mmcblk'): + device['vendor'] = "SD/MMC" + else: + device['vendor'] = "N/A" + if device['model'] is None: + device['model'] = "N/A" + disk_name = f"{device['vendor'].strip()} {device['model'].strip()}" + disk = f"{device['type']} " + disk += f"{disk_name} " + disk += f"{device['size']} " + disk += f"{device['path']}" disks.append(disk) return disks @@ -125,7 +127,7 @@ def select_by_letter(title, ask1, ask2, values): def get_installation_type(): return select_value( ['Install TowerOS-ThinClient', 'Upgrade TowerOS-ThinClient'], - "Do you want to reinstall TowerOS or upgrade an existing installation?", + "Do you want to install TowerOS or upgrade an existing installation?", "Select the installation type", no_columns=True ).split(" ", maxsplit=1)[0].lower() @@ -179,9 +181,11 @@ def check_secure_boot_status(): return True -def get_secure_boot(): +def get_secure_boot(arch): + if arch != 'x86_64': + return False print_title("Secure boot") - with_secure_boot = Confirm.ask("Do you want to set up TowerOS-ThinClient with Secure Boot?") + with_secure_boot = Confirm.ask("Do you want to set up TowerOS-ThinClient with Secure Boot?", default=False) if with_secure_boot and not check_secure_boot_status(): continue_without_secure_boot = Confirm.ask("Do you want to continue without Secure Boot (y), or do you want to reboot (n)?") if continue_without_secure_boot: @@ -225,7 +229,7 @@ def get_keymap(): def get_startw_on_login(): print_title("Start Wayland on login") - return Confirm.ask("Do you want to automatically start the graphical interface on login?") + return Confirm.ask("Do you want to automatically start the graphical interface on login?", default=True) def get_user_information(): @@ -297,17 +301,22 @@ def print_header(): def ask_config(): print_header() confirmed = False - config = {} + arch = run_cmd(['arch']) + config = { + 'ALPINE_BRANCH': THINCLIENT_ALPINE_BRANCH, + 'DEFAULT_PACKAGES': " ".join(THINCLIENT_DEFAULT_PACKAGES[arch]), + } while not confirmed: config['INSTALLATION_TYPE'] = get_installation_type() is_upgrade = config['INSTALLATION_TYPE'] == 'upgrade' config['TARGET_DRIVE'] = get_target_drive(is_upgrade) config['CRYPTKEY_DRIVE'] = get_cryptkey_drive(config['TARGET_DRIVE'], is_upgrade) if not is_upgrade: - config['SECURE_BOOT'] = "true" if get_secure_boot() else "false" + config['KEYBOARD_LAYOUT'], config['KEYBOARD_VARIANT'] = get_keymap() + run_cmd(["setup-keymap", config['KEYBOARD_LAYOUT'], config['KEYBOARD_VARIANT']]) config['LANG'] = get_lang() config['TIMEZONE'] = get_timezone() - config['KEYBOARD_LAYOUT'], config['KEYBOARD_VARIANT'] = get_keymap() + config['SECURE_BOOT'] = "true" if get_secure_boot(arch) else "false" config['STARTW_ON_LOGIN'] = "true" if get_startw_on_login() else "false" config['USERNAME'], config['PASSWORD_HASH'] = get_user_information() config['ROOT_PASSWORD_HASH'] = config['PASSWORD_HASH'] diff --git a/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/install-thinclient.sh b/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/install-thinclient.sh index 1db9d770..b144a597 100644 --- a/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/install-thinclient.sh +++ b/tower-apks/toweros-thinclient/overlay/var/towercomputers/installer/install-thinclient.sh @@ -3,7 +3,7 @@ set -e set -x -ARCH="x86_64" +ARCH="$(arch)" update_passord() { REPLACE="$1:$2:" @@ -104,6 +104,14 @@ set_config_from_root_partition() { fi # set startx on login STARTW_ON_LOGIN="false" # in any case, already present in /home if needed + # get installed packages + ALL_INSTALLED_PACKAGES=$(cat /ROOT/etc/apk/world) + INSTALLED_PACKAGES="" + for package in $ALL_INSTALLED_PACKAGES; do + if [[ ! "$DEFAULT_PACKAGES" == *"$package"* ]]; then + INSTALLED_PACKAGES="$INSTALLED_PACKAGES $package" + fi + done # copy ETH0_MAC if exists if [ -f /ROOT/etc/local.d/eth0_mac ]; then cp /ROOT/etc/local.d/eth0_mac /etc/local.d/eth0_mac @@ -255,16 +263,20 @@ update_live_system() { } +generate_mkinitfs() { + mkdir -p /mnt/etc/mkinitfs/features.d + features="base usb vfat ext4 nvme vmd lvm cryptsetup cryptkey kms" + features="$features ata ide scsi mmc virtio keymap resume" + echo "features=\"$features\"" > /mnt/etc/mkinitfs/mkinitfs.conf +} + + clone_live_system_to_disk() { # backup local config in apkovl ovlfiles=/tmp/ovlfiles lbu package - | tar -C "/mnt" -zxv > $ovlfiles - # generate mkinitfs.conf - mkdir -p /mnt/etc/mkinitfs/features.d - features="ata base ide scsi usb virtio vfat ext4 nvme vmd lvm keymap" - features="$features cryptsetup cryptkey resume" - echo "features=\"$features\"" > /mnt/etc/mkinitfs/mkinitfs.conf + generate_mkinitfs # copy apk keys mkdir -p /mnt/etc/apk/keys/ @@ -278,10 +290,6 @@ clone_live_system_to_disk() { # install packages local apkflags="--quiet --progress --update-cache --clean-protected" - # default alpine packages - local pkgs="alpine-base linux-lts xtables-addons-lts zfs-lts linux-firmware linux-firmware-none" - # toweros packages - pkgs="$pkgs toweros-thinclient" # local repos local repos="$(sed -e 's/\#.*//' "$ROOT"/etc/apk/repositories 2>/dev/null)" local repoflags= @@ -289,12 +297,20 @@ clone_live_system_to_disk() { repoflags="$repoflags --repository $i" done # install packages in /mnt - apk add --root /mnt $apkflags --initdb --overlay-from-stdin --force-overwrite $repoflags $pkgs <$ovlfiles + apk add --root /mnt $apkflags --initdb --overlay-from-stdin --force-overwrite $repoflags $DEFAULT_PACKAGES <$ovlfiles # clean chroot umount /mnt/proc umount /mnt/dev + # configure apk repositories + mkdir -p /mnt/etc/apk + cat < /mnt/etc/apk/repositories +http://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/main +http://dl-cdn.alpinelinux.org/alpine/$ALPINE_BRANCH/community +#http://dl-cdn.alpinelinux.org/alpine/edge/testing +EOF + # disable modloop in /mnt rm -f /mnt/etc/runlevels/sysinit/modloop } @@ -303,36 +319,41 @@ clone_live_system_to_disk() { install_bootloader() { # https://madaidans-insecurities.github.io/guides/linux-hardening.html#result kernel_opts="quiet rootfstype=ext4 slab_nomerge init_on_alloc=1 init_on_free=1 page_alloc.shuffle=1 pti=on vsyscall=none debugfs=off oops=panic module.sig_enforce=1 lockdown=confidentiality mce=0 loglevel=0" - modules="sd-mod,usb-storage,vfat,ext4,nvme,vmd,keymap,kms,lvm" - # add cryptsetup and cryptkey to kernel options - kernel_opts="$kernel_opts cryptroot=$LVM_PARTITION cryptkey=yes cryptdm=lvmcrypt" - modules="$modules,cryptsetup,cryptkey" - - # setup syslinux - sed -e "s:^root=.*:root=$ROOT_PARTITION:" \ - -e "s:^default_kernel_opts=.*:default_kernel_opts=\"$kernel_opts\":" \ - -e "s:^modules=.*:modules=$modules:" \ - /etc/update-extlinux.conf > /mnt/etc/update-extlinux.conf - - dd bs=440 count=1 conv=notrunc if=/usr/share/syslinux/mbr.bin of=$TARGET_DRIVE - - extlinux --install /mnt/boot - chroot /mnt/ update-extlinux - - mkdir -p /mnt/boot/EFI/boot - cp /usr/share/syslinux/efi64/* /mnt/boot/EFI/boot - sed 's/\(initramfs-\|vmlinuz-\)/\/\1/g' /mnt/boot/extlinux.conf > /mnt/boot/EFI/boot/syslinux.cfg - sed -i 's/Alpine\/Linux/TowerOS-ThinClient/g' /mnt/boot/EFI/boot/syslinux.cfg - sed -i 's/Alpine /TowerOS-ThinClient /g' /mnt/boot/EFI/boot/syslinux.cfg - rm -f /mnt/boot/*.c32 - rm -f /mnt/boot/*.sys - rm -f /mnt/boot/extlinux.conf - cp /mnt/boot/EFI/boot/syslinux.efi /mnt/boot/EFI/boot/bootx64.efi + kernel_opts="$kernel_opts root=$ROOT_PARTITION cryptroot=$LVM_PARTITION cryptkey=yes cryptdm=lvmcrypt" + modules="loop,squashfs,sd-mod,usb-storage,vfat,ext4,nvme,vmd,kms,lvm,cryptsetup,cryptkey,keymap" + # x86_64 + if [ "$ARCH" == "x86_64" ]; then + # setup syslinux + sed -e "s:^root=.*:root=$ROOT_PARTITION:" \ + -e "s:^default_kernel_opts=.*:default_kernel_opts=\"$kernel_opts\":" \ + -e "s:^modules=.*:modules=$modules:" \ + /etc/update-extlinux.conf > /mnt/etc/update-extlinux.conf + # write MBR + dd bs=440 count=1 conv=notrunc if=/usr/share/syslinux/mbr.bin of=$TARGET_DRIVE + # install syslinux + extlinux --install /mnt/boot + chroot /mnt/ update-extlinux + mkdir -p /mnt/boot/EFI/boot + cp /usr/share/syslinux/efi64/* /mnt/boot/EFI/boot + sed 's/\(initramfs-\|vmlinuz-\)/\/\1/g' /mnt/boot/extlinux.conf > /mnt/boot/EFI/boot/syslinux.cfg + sed -i 's/Alpine\/Linux/TowerOS-ThinClient/g' /mnt/boot/EFI/boot/syslinux.cfg + sed -i 's/Alpine /TowerOS-ThinClient /g' /mnt/boot/EFI/boot/syslinux.cfg + rm -f /mnt/boot/*.c32 + rm -f /mnt/boot/*.sys + rm -f /mnt/boot/extlinux.conf + cp /mnt/boot/EFI/boot/syslinux.efi /mnt/boot/EFI/boot/bootx64.efi + # RPI + elif [ "$ARCH" == "aarch64" ]; then + # update cmdline.txt + kernel_opts="console=tty1 $kernel_opts" + cmdline="modules=$modules $kernel_opts" + echo "$cmdline" > /mnt/boot/cmdline.txt + fi } install_secure_boot() { - if [ "$SECURE_BOOT" = "true" ]; then + if [ "$SECURE_BOOT" == "true" ] && [ "$ARCH" == "x86_64" ]; then sbctl create-keys cp /mnt/boot/EFI/boot/bootx64.efi /mnt/boot/EFI/boot/bootx64.efi.unsigned sbctl sign /mnt/boot/EFI/boot/bootx64.efi @@ -357,6 +378,12 @@ upgrade_hosts() { runuser -u $USERNAME -- tower upgrade --hosts $(cat /tmp/upgradable-hosts) python $SCRIPT_DIR/askconfiguration.py end-hosts-upgrade fi + if [ -d /mnt/home/$USERNAME/.local/tower/hosts/router ]; then + if [ "$INSTALLED_PACKAGES" != "" ]; then + # re-install thinclient package + runuser -u $USERNAME -- tower install thinclient $INSTALLED_PACKAGES || true + fi + fi # move updated tower configuration back cp -r /mnt/home/$USERNAME/.local/tower /home/$USERNAME/.local/ chown -R $USERNAME:$USERNAME /home/$USERNAME/ @@ -403,7 +430,7 @@ set_configuration() { # INSTALLATION_TYPE, ROOT_PASSWORD_HASH, USERNAME, PASSWORD_HASH, # LANG, TIMEZONE, KEYBOARD_LAYOUT, KEYBOARD_VARIANT, # TARGET_DRIVE, CRYPTKEY_DRIVE, SECURE_BOOT - # STARTW_ON_LOGIN + # STARTW_ON_LOGIN, DEFAULT_PACKAGES, ALPINE_BRANCH python $SCRIPT_DIR/askconfiguration.py source /root/tower.env if [ "$INSTALLATION_TYPE" == "upgrade" ]; then diff --git a/tower-apks/toweros-thinclient/overlay/var/towercomputers/scripts/dev/connect-wifi.sh b/tower-apks/toweros-thinclient/overlay/var/towercomputers/scripts/dev/connect-wifi.sh index 85fc9d0e..fa529440 100644 --- a/tower-apks/toweros-thinclient/overlay/var/towercomputers/scripts/dev/connect-wifi.sh +++ b/tower-apks/toweros-thinclient/overlay/var/towercomputers/scripts/dev/connect-wifi.sh @@ -17,3 +17,10 @@ echo "https://dl-cdn.alpinelinux.org/alpine/latest-stable/community" | sudo tee # restart network sudo rc-service networking restart + +# wait for connection +while ! ping -c 1 -n -w 2 www.google.com &> /dev/null +do + echo "Waiting for connection..." +done +echo "Connected!" diff --git a/tower-apks/toweros-thinclient/world b/tower-apks/toweros-thinclient/world index 2f4b2904..b3345b86 100644 --- a/tower-apks/toweros-thinclient/world +++ b/tower-apks/toweros-thinclient/world @@ -33,13 +33,10 @@ acct-openrc alpine-conf sfdisk fakeroot -syslinux xorriso squashfs-tools mtools dosfstools -grub-efi -efibootmgr abuild agetty runuser @@ -54,8 +51,6 @@ udev-init-scripts udev-init-scripts-openrc mesa-dri-gallium mesa-va-gallium -intel-media-driver -libva-intel-driver font-dejavu font-awesome seatd @@ -115,3 +110,4 @@ gtk-vnc sfwbar sfwbar-doc wl-clipboard +pigz diff --git a/tower-build-cli/tower-build b/tower-build-cli/tower-build index e55b4733..017a96b2 100755 --- a/tower-build-cli/tower-build +++ b/tower-build-cli/tower-build @@ -31,10 +31,15 @@ def parse_arguments(): help="Use `build-tower-image {thinclient|host} --help` to get options list for each image." ) - subparser.add_parser( + thinclient_parser = subparser.add_parser( 'thinclient', help="""Command used to generate thinclient image""" ) + thinclient_parser.add_argument( + '--in-host', + required=False, + help="""Build image inside host""", + ) host_parser = subparser.add_parser( 'host', @@ -60,7 +65,10 @@ def main(): if args.image_name == 'host': buildhost.build_image(args.uncompressed, args.build_dir) elif args.image_name == 'thinclient': - buildthinclient.build_image() + if args.in_host: + buildthinclient.build_image_in_host(args.in_host, verbose=args.verbose) + else: + buildthinclient.build_image() if __name__ == '__main__': sys.exit(main()) diff --git a/tower-cli/towercli/commands/apktunnel.py b/tower-cli/towercli/commands/apktunnel.py new file mode 100644 index 00000000..8b636815 --- /dev/null +++ b/tower-cli/towercli/commands/apktunnel.py @@ -0,0 +1,28 @@ +import logging + +from towerlib import install, sshconf + +logger = logging.getLogger('tower') + + +def add_args(argparser): + help_message = "Open APK tunnel with offline host." + parser = argparser.add_parser( + 'apk-tunnel', + help=help_message, description=help_message + ) + parser.add_argument( + 'host', + help="""Host to install the package on (Required)""", + nargs=1 + ) + + +def check_args(args, parser_error): + config = sshconf.get(args.host[0]) + if config is None: + parser_error("Unknown host.") + + +def execute(args): + install.open_apk_tunnel(args.host[0]) diff --git a/tower-cli/towercli/commands/install.py b/tower-cli/towercli/commands/install.py index 9c52a3aa..a497f10e 100644 --- a/tower-cli/towercli/commands/install.py +++ b/tower-cli/towercli/commands/install.py @@ -19,6 +19,14 @@ def add_args(argparser): help="""Package(s) to install (Required).""", nargs='+' ) + install_parser.add_argument( + # pylint: disable=duplicate-code + '--no-confirm', + help="""Don't ask for confirmation. (Default: False)""", + required=False, + action='store_true', + default=False + ) def check_args(args, parser_error): name = args.host[0] @@ -32,4 +40,4 @@ def check_args(args, parser_error): parser_error(f"Invalid package name:{pkg_name}") def execute(args): - install.install_packages(args.host[0], args.packages) + install.install_packages(args.host[0], args.packages, args.no_confirm) diff --git a/tower-cli/towercli/tower.py b/tower-cli/towercli/tower.py index e387cd37..67c64d10 100755 --- a/tower-cli/towercli/tower.py +++ b/tower-cli/towercli/tower.py @@ -17,6 +17,7 @@ synctime, poweroff, deprovision, + apktunnel, ) @@ -54,6 +55,7 @@ def towercli_parser(): version.add_args(subparser) poweroff.add_args(subparser) deprovision.add_args(subparser) + apktunnel.add_args(subparser) mdhelp.add_args(subparser) # hidden command synctime.add_args(subparser) # hidden command utils.mdhelp.insert_autocompletion_command(parser) # hidden command diff --git a/tower-lib/towerlib/buildhost.py b/tower-lib/towerlib/buildhost.py index e55a11b7..1492c0fc 100644 --- a/tower-lib/towerlib/buildhost.py +++ b/tower-lib/towerlib/buildhost.py @@ -13,7 +13,7 @@ cp, rm, sync, rsync, chown, truncate, mkdir, tar, xz, apk, dd, losetup, abuild_sign, openssl, - scp, ssh, runuser, + scp, ssh, runuser, abuild, doas ) from towerlib import utils, config, sshconf @@ -79,22 +79,30 @@ def build_brcrm_cm4_apk(repo_path): f'runuser -u {USERNAME} -- abuild -r', _cwd=f"{REPO_PATH}/tower-apks/linux-firmware-brcm-cm4" ) - cp(f"{APK_LOCAL_REPOSITORY}/x86_64/linux-firmware-brcm-cm4-1.0-r0.apk", repo_path) + arch = Command('sh')('-c', 'arch').strip() + cp(f"{APK_LOCAL_REPOSITORY}/{arch}/linux-firmware-brcm-cm4-1.0-r0.apk", repo_path) def build_toweros_host_apk(repo_path): + arch = Command('sh')('-c', 'arch').strip() out = {"_out": logger.debug, "_err_to_out": True} - with runuser.bake('-u', USERNAME, '--'): - ssh(BUILDER_HOST, 'sudo apk add alpine-sdk', **out) - ssh(BUILDER_HOST, f'sudo addgroup {USERNAME} abuild', **out) - ssh(BUILDER_HOST, 'rm -rf .abuild tower-apks tower-lib', **out) - scp('-r', f'{REPO_PATH}/tower-apks', f'{BUILDER_HOST}:', **out) - scp('-r', f'{REPO_PATH}/tower-lib', f'{BUILDER_HOST}:', **out) - scp('-r', f'/home/{USERNAME}/.abuild', f'{BUILDER_HOST}:', **out) - ssh(BUILDER_HOST, 'sudo cp .abuild/*.pub /etc/apk/keys/', **out) - ssh(BUILDER_HOST, 'cd tower-apks/toweros-host && abuild -r', **out) - scp(f'{BUILDER_HOST}:packages/tower-apks/{ARCH}/toweros-host-{__version__}-r0.apk', TMP_DIR, **out) - cp(f'{TMP_DIR}/toweros-host-{__version__}-r0.apk', repo_path) + if arch != 'aarch64': + with runuser.bake('-u', USERNAME, '--'): + ssh(BUILDER_HOST, 'sudo apk add alpine-sdk', **out) + ssh(BUILDER_HOST, f'sudo addgroup {USERNAME} abuild', **out) + ssh(BUILDER_HOST, 'rm -rf .abuild tower-apks tower-lib', **out) + scp('-r', f'{REPO_PATH}/tower-apks', f'{BUILDER_HOST}:', **out) + scp('-r', f'{REPO_PATH}/tower-lib', f'{BUILDER_HOST}:', **out) + scp('-r', f'/home/{USERNAME}/.abuild', f'{BUILDER_HOST}:', **out) + ssh(BUILDER_HOST, 'sudo cp .abuild/*.pub /etc/apk/keys/', **out) + ssh(BUILDER_HOST, 'cd tower-apks/toweros-host && abuild -r', **out) + scp(f'{BUILDER_HOST}:packages/tower-apks/{ARCH}/toweros-host-{__version__}-r0.apk', TMP_DIR, **out) + cp(f'{TMP_DIR}/toweros-host-{__version__}-r0.apk', repo_path) + else: + with runuser.bake('-u', USERNAME, '--'): + abuild('-r', _cwd=f"{REPO_PATH}/tower-apks/toweros-host", **out) + with doas: + cp(f'{APK_LOCAL_REPOSITORY}/{arch}/toweros-host-{__version__}-r0.apk', repo_path) def prepare_apk_repos(private_key_path): diff --git a/tower-lib/towerlib/buildthinclient.py b/tower-lib/towerlib/buildthinclient.py index 3ed31a4f..81fa680f 100644 --- a/tower-lib/towerlib/buildthinclient.py +++ b/tower-lib/towerlib/buildthinclient.py @@ -4,33 +4,46 @@ from os.path import join as join_path from shutil import copy as copyfile import sys +import tempfile +import getpass -from towerlib.utils.shell import rm, git, Command, apk, cp, abuild, abuild_sign +from towerlib.utils.shell import rm, git, Command, apk, cp, abuild, abuild_sign, arch, ssh, scp from towerlib.utils.decorators import clitask from towerlib.utils.shell import doas from towerlib.utils.network import download_file +from towerlib.utils.disk import targz_to_image from towerlib.__about__ import __version__ -from towerlib.utils.exceptions import LockException +from towerlib.utils.exceptions import LockException, UnkownHost, TowerException from towerlib.config import THINCLIENT_ALPINE_BRANCH, APK_LOCAL_REPOSITORY, TOWER_BUILDS_DIR +from towerlib import sshconf logger = logging.getLogger('tower') +ARCH = arch().strip() + WORKING_DIR_NAME = 'build-toweros-thinclient-work' WORKING_DIR = join_path(os.path.expanduser('~'), WORKING_DIR_NAME) NOPYFILES_DIR = join_path(os.path.dirname(os.path.abspath(__file__)), 'nopyfiles') -REPO_PATH = join_path(os.path.dirname(os.path.abspath(__file__)), '..', '..') +REPO_PATH = os.path.abspath(join_path(os.path.dirname(__file__), '..', '..')) ALPINE_APORT_REPO = 'https://gitlab.alpinelinux.org/alpine/aports.git' -EDGE_REPO = 'https://dl-cdn.alpinelinux.org/alpine/edge/testing/x86_64/' +TMP_DIR = tempfile.gettempdir() +USERNAME = getpass.getuser() + +EDGE_REPO = f'https://dl-cdn.alpinelinux.org/alpine/edge/testing/{ARCH}/' EDGE_APKS = [ - 'sfwbar-1.0_beta13-r0.apk', - 'sfwbar-doc-1.0_beta13-r0.apk', + 'sfwbar-1.0_beta14-r0.apk', + 'sfwbar-doc-1.0_beta14-r0.apk', ] def wdir(path): return join_path(WORKING_DIR, path) +def sprint(value): + print(value.decode("utf-8", 'ignore') if isinstance(value, bytes) else value, end='', flush=True) + + def prepare_working_dir(): if os.path.exists(WORKING_DIR): raise LockException(f"f{WORKING_DIR} already exists! Is another build in progress? If not, delete this folder and try again.") @@ -52,24 +65,26 @@ def check_abuild_key(): def download_edge_apks(): rm('-rf', APK_LOCAL_REPOSITORY) - makedirs(f'{APK_LOCAL_REPOSITORY}/x86_64') + makedirs(f'{APK_LOCAL_REPOSITORY}/{ARCH}') edge_apks = [] for apk_file in EDGE_APKS: - local_path = f'{APK_LOCAL_REPOSITORY}/x86_64/{apk_file}' + local_path = f'{APK_LOCAL_REPOSITORY}/{ARCH}/{apk_file}' download_file(f'{EDGE_REPO}{apk_file}', local_path) edge_apks.append(local_path) # create index - apk('index', '-o', f'{APK_LOCAL_REPOSITORY}/x86_64/APKINDEX.tar.gz', '--no-warnings', *edge_apks) + apk_index_opts = ['index', '--arch', ARCH, '--rewrite-arch', ARCH, '--allow-untrusted'] + apk(*apk_index_opts, '-o', f'{APK_LOCAL_REPOSITORY}/{ARCH}/APKINDEX.tar.gz', '--no-warnings', *edge_apks) # sign index - abuild_sign(f'{APK_LOCAL_REPOSITORY}/x86_64/APKINDEX.tar.gz') + abuild_sign(f'{APK_LOCAL_REPOSITORY}/{ARCH}/APKINDEX.tar.gz') -@clitask("Prepare Tower CLI APK package...") -def prepare_tower_apk(): +@clitask("Prepare `toweros-thinclient` APK packages...") +def prepare_tower_apks(): with doas: apk('update') # build tower-cli - abuild('-r', _cwd=f"{REPO_PATH}/tower-apks/toweros-thinclient", _err_to_out=True, _out=logger.debug) + abuild('-r', '-f', _cwd=f"{REPO_PATH}/tower-apks/toweros-thinclient", _err_to_out=True, _out=logger.debug) + abuild('-r', '-f', _cwd=f"{REPO_PATH}/tower-apks/toweros-thinclient-builds", _err_to_out=True, _out=logger.debug) @clitask("Building thin client image, be patient...") @@ -77,12 +92,13 @@ def prepare_image(): # download alpine aports form gitlab git('clone', '--depth=1', f'--branch={THINCLIENT_ALPINE_BRANCH[1:]}-stable', ALPINE_APORT_REPO, _cwd=WORKING_DIR) # copy tower custom scripts - copyfile(join_path(NOPYFILES_DIR, 'mkimg.tower.sh'), wdir('aports/scripts')) + copyfile(join_path(NOPYFILES_DIR, f'mkimg.tower-{ARCH}.sh'), wdir('aports/scripts')) copyfile(join_path(NOPYFILES_DIR, 'genapkovl-toweros-thinclient.sh'), wdir('aports/scripts')) with doas: apk('update') Command('sh')( wdir('aports/scripts/mkimage.sh'), + '--arch', ARCH, '--outdir', WORKING_DIR, '--repository', APK_LOCAL_REPOSITORY, '--repository', f'http://dl-cdn.alpinelinux.org/alpine/{THINCLIENT_ALPINE_BRANCH}/main', @@ -92,16 +108,27 @@ def prepare_image(): _err_to_out=True, _out=logger.debug, _cwd=WORKING_DIR ) - image_src_path = wdir(f"alpine-tower-{__version__}-x86_64.iso") + image_extension = 'iso' if ARCH == 'x86_64' else 'tar.gz' + image_src_path = wdir(f"alpine-tower-{__version__}-{ARCH}.{image_extension}") image_dest_path = join_path( TOWER_BUILDS_DIR, - f'toweros-thinclient-{__version__}-x86_64.iso' + f'toweros-thinclient-{__version__}-{ARCH}.{image_extension}' ) with doas: cp(image_src_path, image_dest_path) return image_dest_path +def convert_archive_to_image(image_path): + image_extension = 'iso' if ARCH == 'x86_64' else 'tar.gz' + if image_extension == 'iso': + return image_path + # convert to image + with doas: + targz_to_image(image_path) + return image_path.replace('.tar.gz', '.img.gz') + + @clitask("Building TowserOS-ThinClient image...", timer_message="TowserOS-ThinClient image built in {0}.", task_parent=True) def build_image(): image_path = None @@ -109,9 +136,68 @@ def build_image(): check_abuild_key() prepare_working_dir() download_edge_apks() - prepare_tower_apk() + prepare_tower_apks() image_path = prepare_image() + image_path = convert_archive_to_image(image_path) finally: cleanup() if image_path: logger.info("Image ready: %s", image_path) + + +@clitask("Preparing host {0} for build...") +def prepare_host_for_build(build_host): + out = {"_out": logger.debug, "_err_to_out": True} + # clean previous install + ssh(build_host, 'rm -rf toweros .abuild packages', **out) + # copy toweros repo + scp('-r', REPO_PATH, f'{build_host}:', **out) + # install apk keys + scp('-r', f"/home/{USERNAME}/.abuild", f'{build_host}:', **out) + ssh(build_host, "sudo cp .abuild/*.pub /etc/apk/keys/", **out) + # install packages + build_depends = [ + "alpine-sdk", "xz", "rsync", "perl-utils", "musl-locales", + "py3-pip", "py3-requests", "py3-rich", "cairo", "cairo-dev", "python3-dev", + "gobject-introspection", "gobject-introspection-dev", + "xsetroot", "losetup", "squashfs-tools", "xorriso", "pigz", "mtools", + ] + ssh(build_host, "sudo apk update", **out) + ssh(build_host, f"sudo apk add {' '.join(build_depends)}", **out) + ssh(build_host, f"sudo addgroup {USERNAME} abuild", **out) + # install tower-lib abd tower-cli + ssh(build_host, "sudo pip install -e toweros/tower-lib --break-system-packages", **out) + ssh(build_host, "sudo pip install -e toweros/tower-cli --break-system-packages --no-deps", **out) + + +@clitask("Transferring image from host {0} to thin client...") +def copy_image_from_host(build_host): + out = {"_out": logger.debug, "_err_to_out": True} + host_arch = ssh(build_host, "arch").strip() + image_extension = 'iso' if host_arch == 'x86_64' else 'img.gz' + archive_name = f"toweros-thinclient-{__version__}-{host_arch}.{image_extension}" + archive_dest_path = join_path(TOWER_BUILDS_DIR, archive_name) + scp(f"{build_host}:{archive_dest_path}", TMP_DIR, **out) + with doas: + cp(join_path(TMP_DIR, archive_name), TOWER_BUILDS_DIR) + return archive_dest_path + + +@clitask("Building TowserOS-ThinClient image in {0}...", timer_message="TowserOS-ThinClient image built in {0}.", task_parent=True) +def build_image_in_host(build_host, verbose=False): + if not sshconf.exists(build_host): + raise UnkownHost(f"Host {build_host} not found.") + if not sshconf.is_up(build_host): + raise TowerException(f"Host {build_host} is not up.") + # prepare host + prepare_host_for_build(build_host) + # build image + verbose_opt = "--verbose" if verbose else "" + ssh( + '-t', build_host, + f"cd toweros/tower-build-cli && ./tower-build {verbose_opt} thinclient", + _err=sprint, _out=sprint, _in=sys.stdin, + _out_bufsize=0, _err_bufsize=0, + ) + image_path = copy_image_from_host(build_host) + logger.info("Image ready: %s", image_path) diff --git a/tower-lib/towerlib/config.py b/tower-lib/towerlib/config.py index fa993608..3153e650 100644 --- a/tower-lib/towerlib/config.py +++ b/tower-lib/towerlib/config.py @@ -38,6 +38,11 @@ THINCLIENT_ALPINE_BRANCH = "v3.19" ALPINE_RPI_URL = "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/aarch64/alpine-rpi-3.19.0-aarch64.tar.gz" ALPINE_RPI_CHECKSUM = "5621e7e597c3242605cd403a0a9109ec562892a6c8a185852b6b02ff88f5503c" +HOST_DEFAULT_PACKAGES = 'toweros-host'.split(" ") +THINCLIENT_DEFAULT_PACKAGES = { + 'x86_64': 'toweros-thinclient toweros-thinclient-builds alpine-base linux-lts xtables-addons-lts zfs-lts syslinux intel-media-driver libva-intel-driver linux-firmware linux-firmware-none'.split(" "), + 'aarch64': 'toweros-thinclient toweros-thinclient-builds alpine-base linux-rpi raspberrypi-bootloader linux-firmware-brcm'.split(" "), +} VNC_VIEWER_CSS = """ headerbar { diff --git a/tower-lib/towerlib/install.py b/tower-lib/towerlib/install.py index 088374bc..a383c215 100644 --- a/tower-lib/towerlib/install.py +++ b/tower-lib/towerlib/install.py @@ -1,16 +1,17 @@ -import os import logging import sys import time +import signal from rich.prompt import Confirm from rich.text import Text +from rich import print as rprint -from towerlib.utils.shell import ssh, scp, rm, Command, ErrorReturnCode +from towerlib.utils.shell import ssh, Command, ErrorReturnCode from towerlib.utils import clitask -from towerlib.utils.menu import add_installed_package, get_installed_packages -from towerlib.sshconf import is_online_host -from towerlib.utils.exceptions import LockException, TowerException +from towerlib.utils.menu import copy_desktop_files +from towerlib.sshconf import is_online_host, get_saved_packages +from towerlib.utils.exceptions import TowerException from towerlib import sshconf, config logger = logging.getLogger('tower') @@ -21,26 +22,13 @@ ] LOCAL_TUNNELING_PORT = 8666 +signal.signal(signal.SIGTRAP, signal.default_int_handler) +signal.signal(signal.SIGHUP, signal.default_int_handler) def sprint(value): print(value.decode("utf-8", 'ignore') if isinstance(value, bytes) else value, end='', flush=True) -def prepare_repositories_file(host): - file_name = os.path.join(os.path.expanduser('~'), f'repositories.offline.{host}') - # use temporary file as lock file - if os.path.exists(file_name): - raise LockException(f"f{file_name} already exists! Is another install in progress? If not, delete this file and try again.") - # generate temporary apk repositories - with open(file_name, 'w', encoding="UTF-8") as file_pointer: - for repo in APK_REPOS_URL: - file_pointer.write(f"{repo}\n") - # copy apk repositories in offline host - if host != 'thinclient': - scp(file_name, f"{host}:~/") - rm('-f', file_name) - - def offline_cmd(host, cmd): if host == 'thinclient': Command('sh')('-c', cmd) @@ -50,8 +38,6 @@ def offline_cmd(host, cmd): @clitask("Preparing installation...") def prepare_offline_host(host): - # prepare apk repositories in offline host - prepare_repositories_file(host) # add repo host in /etc/hosts offline_cmd(host, 'sudo cp /etc/hosts /etc/hosts.bak') offline_cmd(host, f"echo '127.0.0.1 {APK_REPOS_HOST}\n' | sudo tee /etc/hosts") @@ -61,11 +47,6 @@ def prepare_offline_host(host): def cleanup_offline_host(host): - # remove temporary apk repositories in thinclient - file_name = f'~/repositories.offline.{host}' - if host == 'thinclient': - file_name = os.path.expanduser(file_name) - offline_cmd(host, f"rm -f {file_name}") # restore /etc/hosts offline_cmd(host, "sudo mv /etc/hosts.bak /etc/hosts") # clean iptables @@ -85,7 +66,6 @@ def cleanup(host): cleanup_offline_host(host) -@clitask("Installing {1} in {0}...", task_parent=True) def install_in_online_host(host, packages): # we just need to run apk with ssh... try: @@ -96,7 +76,7 @@ def install_in_online_host(host, packages): _out_bufsize=0, _err_bufsize=0, ) for package in packages: - add_installed_package(host, package) + copy_desktop_files(host, package) except ErrorReturnCode as exc: raise TowerException(f"Error while installing packages in {host}") from exc @@ -111,7 +91,6 @@ def open_router_tunnel(): time.sleep(1) -@clitask("Installing {1} in {0}...", task_parent=True) def install_in_offline_host(host, packages): try: prepare_offline_host(host) @@ -123,7 +102,7 @@ def install_in_offline_host(host, packages): ssh( '-R', f'4443:127.0.0.1:{LOCAL_TUNNELING_PORT}', '-t', host, - f"sudo apk --repositories-file ~/repositories.offline.{host} --progress -v add {' '.join(packages)}", + f"sudo apk --progress -v add {' '.join(packages)}", _err=sprint, _out=sprint, _in=sys.stdin, _out_bufsize=0, _err_bufsize=0, ) @@ -131,14 +110,13 @@ def install_in_offline_host(host, packages): error = True # error in remote host is already displayed if not error: for package in packages: - add_installed_package(host, package) + copy_desktop_files(host, package) finally: cleanup(host) if error: raise TowerException(f"Error while installing packages in {host}") -@clitask("Installing {0} in thin client...", task_parent=True) def install_in_thinclient(packages): error = False try: @@ -146,8 +124,7 @@ def install_in_thinclient(packages): open_router_tunnel() logger.info("Running apk in thinclient...") try: - repo_file = os.path.expanduser('~/repositories.offline.thinclient') - apk_cmd = f"sudo apk --repositories-file {repo_file} --progress add {' '.join(packages)}" + apk_cmd = f"sudo apk --progress add {' '.join(packages)}" Command('sh')('-c', apk_cmd, _err_to_out=True, _out=sprint, _in=sys.stdin, @@ -166,18 +143,26 @@ def can_install(host): raise TowerException(f"`{host}` is down. Please start it first.") if (host == "thinclient" or not sshconf.is_online_host(host)) and not sshconf.exists(config.ROUTER_HOSTNAME): raise TowerException(f"`{host}` is an offline host and `{config.ROUTER_HOSTNAME}` host was not found. Please provision it first.") + if not sshconf.is_up(config.ROUTER_HOSTNAME): + raise TowerException(f"`{config.ROUTER_HOSTNAME}` is down. Please start it first.") -def install_packages(host, packages): - can_install(host) +def display_install_warning(host): if host == 'thinclient': confirmation = Text("This is a *dangerous operation*. Packages should normally be installed only on hosts. Are you sure you want to install this package directly on the thin client?", style='red') if not Confirm.ask(confirmation): - return + raise TowerException("Installation aborted.") if host == 'router': confirmation = Text("This is a *dangerous operation*. Packages should normally be installed only on other hosts. Are you sure you want to install this package on the router?", style='red') if not Confirm.ask(confirmation): - return + raise TowerException("Installation aborted.") + + +@clitask("Installing {1} in {0}...", task_parent=True) +def install_packages(host, packages, no_confirm=False): + can_install(host) + if not no_confirm: + display_install_warning(host) if host == 'thinclient': install_in_thinclient(packages) elif is_online_host(host): @@ -188,6 +173,27 @@ def install_packages(host, packages): def reinstall_all_packages(host): can_install(host) - packages = get_installed_packages(host) + packages = get_saved_packages(host) if packages: - install_packages(host, packages) + install_packages(host, packages, no_confirm=True) + + +@clitask("Opening APK tunnel with {0}...", task_parent=True) +def open_apk_tunnel(host): + if host != "thinclient" and sshconf.is_online_host(host): + raise TowerException(f"`{host}` is an online host. You can use `apk` command directly in `{host}`.") + can_install(host) + display_install_warning(host) + try: + prepare_offline_host(host) + open_router_tunnel() + if host != "thinclient": + ssh('-R', f'4443:127.0.0.1:{LOCAL_TUNNELING_PORT}', '-N', host, _bg=True, _bg_exc=False) + message = f"APK tunnel opened. You can use `apk` command in host `{host}` with `ssh {host} sudo apk ...`.\nPress Ctrl+C to close it." + rprint(Text(message, style="green bold")) + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + cleanup(host) diff --git a/tower-lib/towerlib/nopyfiles/genapkovl-toweros-thinclient.sh b/tower-lib/towerlib/nopyfiles/genapkovl-toweros-thinclient.sh index f5c500d8..b527799c 100755 --- a/tower-lib/towerlib/nopyfiles/genapkovl-toweros-thinclient.sh +++ b/tower-lib/towerlib/nopyfiles/genapkovl-toweros-thinclient.sh @@ -39,10 +39,27 @@ tty6::respawn:/sbin/getty 38400 tty6 EOF mkdir -p "$tmp"/etc/apk -cat < "$tmp"/etc/apk/world + +if [ "$(arch)" == "aarch64" ]; then + cat < "$tmp"/etc/apk/world +alpine-base +raspberrypi-bootloader +linux-firmware-brcm +toweros-thinclient +EOF +else + cat < "$tmp"/etc/apk/world alpine-base +syslinux +intel-media-driver +libva-intel-driver +linux-firmware +linux-firmware-none +grub-efi +efibootmgr toweros-thinclient EOF +fi rc_add() { mkdir -p "$tmp"/etc/runlevels/"$2" @@ -53,4 +70,3 @@ rc_add modloop sysinit # generate apk overlay tar -c -C "$tmp" ./ | gzip -9n > $HOSTNAME.apkovl.tar.gz -#cp $HOSTNAME.apkovl.tar.gz ~/ diff --git a/tower-lib/towerlib/nopyfiles/mkimg.tower-aarch64.sh b/tower-lib/towerlib/nopyfiles/mkimg.tower-aarch64.sh new file mode 100755 index 00000000..1ff594a4 --- /dev/null +++ b/tower-lib/towerlib/nopyfiles/mkimg.tower-aarch64.sh @@ -0,0 +1,29 @@ +set -x + +profile_tower() { + profile_base + profile_abbrev="tower" + title="TowerOS" + desc="TowerOS for thin clients." + image_ext="tar.gz" + + arch="aarch64" + kernel_flavors="rpi" + kernel_cmdline="console=tty1" + initfs_features="base squashfs mmc usb kms dhcp https vmd lvm cryptsetup cryptkey" + kernel_addons="zfs" + grub_mod= + hostname="rpi" + + apkovl="aports/scripts/genapkovl-toweros-thinclient.sh" + local _k _a + for _k in $kernel_flavors; do + apks="$apks linux-$_k" + for _a in $kernel_addons; do + apks="$apks $_a-$_k" + done + done + apks="$apks raspberrypi-bootloader linux-firmware-brcm" + apks="$apks toweros-thinclient toweros-thinclient-builds" +} + diff --git a/tower-lib/towerlib/nopyfiles/mkimg.tower-x86_64.sh b/tower-lib/towerlib/nopyfiles/mkimg.tower-x86_64.sh new file mode 100755 index 00000000..244c70e3 --- /dev/null +++ b/tower-lib/towerlib/nopyfiles/mkimg.tower-x86_64.sh @@ -0,0 +1,26 @@ +set -x + +profile_tower() { + profile_base + profile_abbrev="tower" + title="TowerOS" + desc="TowerOS for thin clients." + image_ext="iso" + output_format="iso" + + arch="x86 x86_64" + kernel_addons="xtables-addons zfs" + boot_addons="amd-ucode intel-ucode" + initrd_ucode="/boot/amd-ucode.img /boot/intel-ucode.img" + + apkovl="aports/scripts/genapkovl-toweros-thinclient.sh" + local _k _a + for _k in $kernel_flavors; do + apks="$apks linux-$_k" + for _a in $kernel_addons; do + apks="$apks $_a-$_k" + done + done + apks="$apks syslinux linux-firmware linux-firmware-none intel-media-driver libva-intel-driver grub-efi efibootmgr" + apks="$apks toweros-thinclient toweros-thinclient-builds" +} diff --git a/tower-lib/towerlib/nopyfiles/mkimg.tower.sh b/tower-lib/towerlib/nopyfiles/mkimg.tower.sh deleted file mode 100755 index c537d3e2..00000000 --- a/tower-lib/towerlib/nopyfiles/mkimg.tower.sh +++ /dev/null @@ -1,29 +0,0 @@ -set -x - -profile_tower() { - profile_base - profile_abbrev="tower" - title="TowerOS" - desc="TowerOS for thin clients." - image_ext="iso" - output_format="iso" - arch="x86 x86_64" - kernel_addons="xtables-addons zfs" - boot_addons="amd-ucode intel-ucode" - initrd_ucode="/boot/amd-ucode.img /boot/intel-ucode.img" - apkovl="aports/scripts/genapkovl-toweros-thinclient.sh" - local _k _a - for _k in $kernel_flavors; do - apks="$apks linux-$_k" - for _a in $kernel_addons; do - apks="$apks $_a-$_k" - done - done - apks="$apks linux-firmware linux-firmware-none" - apks="$apks toweros-thinclient" - # alpine-base busybox chrony dhcpcd doas e2fsprogs - # kbd-bkeymaps network-extras openntpd openssl openssh - # tzdata wget tiny-cloud-alpine linux-lts xtables-addons-lts - # zfs-lts linux-firmware linux-firmware-none tower-cli -} - diff --git a/tower-lib/towerlib/provision.py b/tower-lib/towerlib/provision.py index 578bfd80..6e1d5cfd 100644 --- a/tower-lib/towerlib/provision.py +++ b/tower-lib/towerlib/provision.py @@ -111,6 +111,7 @@ def prepare_host_config(host, args): 'COLOR': host_color, 'INSTALLATION_TYPE': "install", 'ALPINE_BRANCH': config.HOST_ALPINE_BRANCH, + 'DEFAULT_PACKAGES': ' '.join(config.HOST_DEFAULT_PACKAGES), } @@ -334,6 +335,8 @@ def upgrade_hosts(hosts, args): return for host in hosts: + # backup installed packages + sshconf.save_installed_packages(host) # copy TowerOS-Host image to boot device buildhost.burn_image_in_host( host, diff --git a/tower-lib/towerlib/sshconf.py b/tower-lib/towerlib/sshconf.py index c7fb4bbe..06269ee8 100644 --- a/tower-lib/towerlib/sshconf.py +++ b/tower-lib/towerlib/sshconf.py @@ -7,7 +7,7 @@ from rich.text import Text from sshconf import read_ssh_config, empty_ssh_config_file -from towerlib.utils.shell import ssh, ErrorReturnCode, sed, touch, Command +from towerlib.utils.shell import ssh, ErrorReturnCode, sed, touch, Command, cat, arch from towerlib.utils import clitask from towerlib.utils.exceptions import DiscoveringTimeOut, UnkownHost, InvalidColor from towerlib.__about__ import __version__ @@ -20,6 +20,8 @@ KNOWN_HOSTS_PATH, COLORS, ROUTER_HOSTNAME, + HOST_DEFAULT_PACKAGES, + THINCLIENT_DEFAULT_PACKAGES ) logger = logging.getLogger('tower') @@ -166,13 +168,14 @@ def status(host = None, full = True): host_info['memory-total'] = memory_available host_info['cpu-usage'] = str(round(100 - float(ssh(host, 'mpstat').strip().split("\n")[-1].split(" ")[-1]), 2)) + "%" host_info['cpu-temperature'] = inxi_info[inxi_info.index('cpu: ') + 5:inxi_info.index(' mobo: ')].strip() + host_info['packages-installed'] = ', '.join(get_installed_packages(host)) else: host_info['system'] = 'N/A' host_info['memory-usage'] = 'N/A' host_info['memory-total'] = 'N/A' host_info['cpu-usage'] = 'N/A' host_info['cpu-temperature'] = 'N/A' - host_info['packages-installed'] = ', '.join(get_installed_packages(host)) + host_info['packages-installed'] = 'N/A' return host_info return [status(host, False) for host in hosts()] @@ -317,16 +320,31 @@ def get_hex_host_color(host): def get_installed_packages(host): - apk_world = os.path.join(TOWER_DIR, 'hosts', host, 'world') - if os.path.exists(apk_world): - return open(apk_world, 'r', encoding="UTF-8").read().strip().split("\n") + if host == "thinclient": + thinclient_world = cat('/etc/apk/world').strip().split("\n") + default_packages = THINCLIENT_DEFAULT_PACKAGES[arch().strip()] + return [package for package in thinclient_world if package not in default_packages] + host_world = ssh(host, 'cat /etc/apk/world').strip().split("\n") + return [package for package in host_world if package not in HOST_DEFAULT_PACKAGES] + + +def get_saved_packages(host): + host_bakup_path = os.path.join(TOWER_DIR, 'hosts', host, 'world') + thinclient_backup_path = os.path.join(TOWER_DIR, 'thinclient_world') + backup_path = host_bakup_path if host != "thinclient" else thinclient_backup_path + if os.path.exists(backup_path): + return open(backup_path, 'r', encoding="UTF-8").read().strip().split("\n") return [] -def save_installed_packages(host, installed_packages): - apk_world = os.path.join(TOWER_DIR, 'hosts', host, 'world') - with open(apk_world, 'w', encoding="UTF-8") as file_pointer: - file_pointer.write("\n".join(installed_packages)) +@clitask("Saving installed package in {0}...") +def save_installed_packages(host): + host_bakup_path = os.path.join(TOWER_DIR, 'hosts', host, 'world') + thinclient_backup_path = os.path.join(TOWER_DIR, 'thinclient_world') + backup_path = host_bakup_path if host != "thinclient" else thinclient_backup_path + current_world = get_installed_packages(host) + with open(backup_path, 'w', encoding="UTF-8") as file_pointer: + file_pointer.write("\n".join(current_world)) @clitask("Syncing offline host time with `router`...") diff --git a/tower-lib/towerlib/utils/decorators.py b/tower-lib/towerlib/utils/decorators.py index ef16e176..e91c171a 100644 --- a/tower-lib/towerlib/utils/decorators.py +++ b/tower-lib/towerlib/utils/decorators.py @@ -7,6 +7,7 @@ from rich import print as rich_print from towerlib.utils.shell import doas +from towerlib.config import TOWER_BUILDS_DIR logger = logging.getLogger('tower') @@ -41,7 +42,9 @@ def join_list(item_list): def format_arg(arg): if isinstance(arg, list): return join_list(arg) - return f"`{arg}`" + value = str(arg) + value = value.replace(f'{TOWER_BUILDS_DIR}/', '') + return f"`{value}`" def clitask(message=None, timer=True, timer_message="Done in {0}", sudo=False, task_parent=False): def decorator(function): diff --git a/tower-lib/towerlib/utils/disk.py b/tower-lib/towerlib/utils/disk.py index 9eb0608d..1276dff9 100644 --- a/tower-lib/towerlib/utils/disk.py +++ b/tower-lib/towerlib/utils/disk.py @@ -2,11 +2,15 @@ import logging import time import os +import tempfile +import uuid -from towerlib.utils.shell import lsblk, umount, ErrorReturnCode, doas +from towerlib.utils.shell import lsblk, umount, ErrorReturnCode, doas, Command +from towerlib.utils.decorators import clitask logger = logging.getLogger('tower') +TMP_DIR = tempfile.gettempdir() def unmount_all(device): result = lsblk('-J', '-T', device) @@ -64,3 +68,29 @@ def select_boot_device(): def select_install_device(): return select_device("install") + + +def sh_cmd(cmd, **kwargs): + return Command('sh')('-c', cmd, **kwargs) + +def folder_to_image(folder, image_path): + out = {"_out": logger.debug, "_err_to_out": True} + sh_cmd(f"sync {folder}") + get_size_cmd = f"du -L -k -s {folder} | awk '{{print $1 + 16384}}'" + image_size = int(sh_cmd(get_size_cmd)) + sh_cmd(f"dd if=/dev/zero of={image_path} bs=1M count={image_size // 1024}") + sh_cmd(f"mformat -i {image_path} -N 0 ::", **out) + sh_cmd(f"mcopy -s -i {image_path} {folder}/* {folder}/.alpine-release ::", **out) + sh_cmd(f"pigz -v -f -9 {image_path}", **out) + +@clitask("Converting archive {} to image disk...") +def targz_to_image(targz_path): + archive_name = os.path.basename(targz_path) + archive_folder = os.path.dirname(targz_path) + image_name = archive_name.replace(".tar.gz", ".img") + image_path = os.path.join(archive_folder, image_name) + tmp_folder = f"/{TMP_DIR}/tower-build-{uuid.uuid4()}" + sh_cmd(f"mkdir -p {tmp_folder}") + sh_cmd(f"tar -xpf {targz_path} -C {tmp_folder}") + folder_to_image(tmp_folder, image_path) + sh_cmd(f"rm -rf {tmp_folder}") diff --git a/tower-lib/towerlib/utils/menu.py b/tower-lib/towerlib/utils/menu.py index df679733..d91e78c4 100644 --- a/tower-lib/towerlib/utils/menu.py +++ b/tower-lib/towerlib/utils/menu.py @@ -4,7 +4,7 @@ from towerlib.utils.shell import ssh, mkdir, sed, scp, mv, Command from towerlib.utils.decorators import clitask -from towerlib.sshconf import get_host_color_name, hosts, get_installed_packages, save_installed_packages, status as get_status +from towerlib.sshconf import get_host_color_name, hosts, status as get_status from towerlib.config import TOWER_DIR, DESKTOP_FILES_DIR def restart_sfwbar(): @@ -46,14 +46,6 @@ def copy_desktop_files(host, package): Command('sh')('-c', f"gtk-update-icon-cache -f -t /usr/{share_icon_folder} || true") restart_sfwbar() -def add_installed_package(host, package): - # save package in host world - installed_packages = get_installed_packages(host) - if package not in installed_packages: - installed_packages.append(package) - save_installed_packages(host, installed_packages) - # copy desktop files from host to thinclient - copy_desktop_files(host, package) STATUS_KEYS = { "name": "Name", diff --git a/tower-lib/towerlib/utils/shell.py b/tower-lib/towerlib/utils/shell.py index 6f3e1ce7..9933a24b 100644 --- a/tower-lib/towerlib/utils/shell.py +++ b/tower-lib/towerlib/utils/shell.py @@ -9,7 +9,7 @@ xsetroot, mcookie, waypipe, argparse_manpage, locale as getlocale, - doas, runuser, + doas, runuser, arch, ) # pylint: enable=import-error,unused-import,no-name-in-module