Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:seife:Factory
yomi-formula
yomi-0.0.1+git.1630589391.4557cfd.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File yomi-0.0.1+git.1630589391.4557cfd.obscpio of Package yomi-formula
07070100000000000081A40000000000000000000000016130D1CF0000050A000000000000000000000000000000000000002D00000000yomi-0.0.1+git.1630589391.4557cfd/.gitignore# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # emacs *~ 07070100000001000081A40000000000000000000000016130D1CF00002C5D000000000000000000000000000000000000002A00000000yomi-0.0.1+git.1630589391.4557cfd/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 07070100000002000081A40000000000000000000000016130D1CF0000B7E4000000000000000000000000000000000000002C00000000yomi-0.0.1+git.1630589391.4557cfd/README.md# Yomi - Yet one more installer Table of contents ================= * [Yomi - Yet one more installer](#yomi---yet-one-more-installer) * [What is Yomi](#what-is-yomi) * [Overview](#overview) * [Installing and configuring salt-master](#installing-and-configuring-salt-master) * [Other ways to install salt-master](#other-ways-to-install-salt-master) * [Looking for the pillar](#looking-for-the-pillar) * [Enabling auto-sign](#enabling-auto-sign) * [Salt API](#salt-api) * [The Yomi formula](#the-yomi-formula) * [Looking for the pillar in Yomi](#looking-for-the-pillar-in-yomi) * [Enabling auto-sing in Yomi](#enabling-auto-sing-in-yomi) * [Salt API in Yomi](#salt-api-in-yomi) * [Real time monitoring in Yomi](#real-time-monitoring-in-yomi) * [Booting a new machine](#booting-a-new-machine) * [The ISO image](#the-iso-image) * [PXE Boot](#pxe-boot) * [Finding the master node](#finding-the-master-node) * [Setting the minion ID](#setting-the-minion-id) * [Adding user provided configuration](#adding-user-provided-configuration) * [Container](#container) * [Basic operations](#basic-operations) * [Getting hardware information](#getting-hardware-information) * [Configuring the pillar](#configuring-the-pillar) * [Cleaning the disks](#cleaning-the-disks) * [Applying the yomi state](#applying-the-yomi-state) * [Pillar reference for Yomi](#pillar-reference-for-yomi) * [config section](#config-section) * [partitions section](#partitions-section) * [lvm section](#lvm-section) * [raid section](#raid-section) * [filesystems section](#filesystems-section) * [bootloader section](#bootloader-section) * [software section](#software-section) * [suseconnect section](#suseconnect-section) * [salt-minion section](#salt-minion-section) * [services section](#services-section) * [networks section](#networks-section) * [users section](#users-section) # What is Yomi Yomi (yet one more installer) is a new proposal for an installer for the [open]SUSE family. It is designed as a [SaltStack](https://www.saltstack.com/) state, and expected to be used in situations were unattended installations for heterogeneous nodes is required, and where some bits of intelligence in the configuration file can help to customize the installation. Being also a Salt state makes the installation process one more step during the provisioning stage, making on Yomi a good candidate for integration in any workflow were SaltStack is used. # Overview To execute Yomi we need a modern version of Salt, as we need special features are only on the [master](https://github.com/saltstack/salt/tree/master) branch of Salt. Technically we can use the last released version of Salt for salt-master, but for the minions we need the most up-to-date version. The good news is that most of the patches are currently merged in the openSUSE package of Salt. Yomi is developed in [OBS](https://build.opensuse.org/project/show/systemsmanagement:yomi), and actually consists on two components: * [yomi-formula](https://build.opensuse.org/package/show/systemsmanagement:yomi/yomi-formula): contains the Salt states and modules requires to drive an installation. The [source code](https://github.com/openSUSE/yomi) of the project in available under the openSUSE group in GitHub. * [openSUSE-Tubleweed-Yomi](https://build.opensuse.org/package/show/systemsmanagement:yomi/openSUSE-Tumbleweed-Yomi): is the image that can be used too boot the new nodes, that includes the `salt-minion` service already configured. There are two versions of this image, one that is used as a LiveCD image and other designed to be used from a PXE Boot server. The installation process of Yomi will require: * Install and configure the [`salt-master`](#installing-and-configuring-salt-master) service. * Install the [`yomi-formula`](#the-yomi-formula) package. * Prepare the [pillar](#pillar-in-yomi) for the new installations. * Boot the new systems with the [ISO image](#the-iso-image) or via [PXE boot](#pxe-boot) Currently Yomi support the installation under x86_64 and ARM64 (aarch64) with EFI. # Installing and configuring salt-master SaltStack can be deployed with different architectures. The recommended one will require the `salt-master` service. ```bash zypper in salt-master systemctl enable --now salt-master.service ``` ## Other ways to install salt-master For different ways of installation, read the [official documentation](https://docs.saltstack.com/en/latest/topics/installation/index.html). For example, for development purposes installing it inside a virtual environment can be a good idea: ```bash python3 -mvenv venv source venv/bin/activate pip install --upgrade pip pip install salt # Create the basic layout and config files mkdir -p venv/etc/salt/pki/{master,minion} \ venv/etc/salt/autosign_grains \ venv/var/cache/salt/master/file_lists/roots cat <<EOF > venv/etc/salt/master root_dir: $(pwd)/venv file_roots: base: - $(pwd)/srv/salt pillar_roots: base: - $(pwd)/srv/pillar EOF ``` ## Looking for the pillar Salt pillar are the data that the Salt states use to decide the actions that needs to be done. For example, in the case of Yomi the typical data will be the layout of the hard disks, the software patterns that will be installed, or the users that will be created. For a complete explanation of the pillar required by Yomi, check the section [Pillar in Yomi](#pillar-in-yomi) By default Salt will search the states in `/srv/salt`, and the pillar in `/srv/pillar`, as established by `file_roots` and `pillar_roots` parameters in the default configuration file (`/etc/salt/master`). To indicate a different place where to find the pillar, we can add a new snippet in the `/etc/salt/master.d` directory: ```bash cat <<EOF > /etc/salt/master.d/pillar.conf pillar_roots: base: - /srv/pillar - /usr/share/yomi/pillar EOF ``` The `yomi-formula` package already contains an example of such configuration. Check section [Looking for the pillar in Yomi](#looking-for-the-pillar-in-yomi) ## Enabling auto-sign To simplify the discovery and key management of the minions, we can use the auto-sign feature of Salt. To do that we need to add a new file in `/etc/salt/master.d`. ```bash echo "autosign_grains_dir: /etc/salt/autosign_grains" > \ /etc/salt/master.d/autosign.conf ``` The Yomi ISO image available in Factory already export some UUIDs generated for each minion, so we need to list into the master all the possible valid UUIDs. ```bash mkdir -p /etc/salt/autosign_grains for i in $(seq 0 9); do echo $(uuidgen --md5 --namespace @dns --name http://opensuse.org/$i) done > /etc/salt/autosign_grains/uuid ``` The `yomi-formula` package already contains an example of such configuration. Check section [Enabling auto-sing in Yomi](#enabling-auto-sing-in-yomi) ## Salt API The `salt-master` service can be accessed via a REST API, provided by an external tool that needs to be enabled. ```bash zypper in salt-api systemctl enable --now salt-api.service ``` There are different options to configure the `salt-api` service, but is safe to choose `CherryPy` as a back-end to serve the requests of Salt API. We need to configure this service to listen to one port, for example 8000, and to associate an authorization mechanism. Read the Salt documentation about this topic for different options. ```bash cat <<EOF > /etc/salt/master.d/salt-api.conf rest_cherrypy: port: 8000 debug: no disable_ssl: yes EOF cat <<EOF > /etc/salt/master.d/eauth.conf external_auth: file: ^filename: /etc/salt/user-list.txt salt: - .* - '@wheel' - '@runner' - '@jobs' EOF echo "salt:linux" > /etc/salt/user-list.txt ``` The `yomi-formula` package already contains an example of such configuration. Check section [Salt API in Yomi](#salt-api-in-yomi) # The Yomi formula The states and modules required by Salt to drive an installation can be installed where the `salt-master` resides: ```bash zypper in yomi-formula ``` This package will install the states in `/usr/share/salt-formulas/states`, some pillar examples in `/usr/share/yomi/pillar` and configuration files in `/usr/share/yomi`. ## Looking for the pillar in Yomi Yomi expect from the pillar to be a normal YAML document, optionally generated by a Jinja template, as is usual in Salt. The schema of the pillar is described in the section [Pillar reference for Yomi](#pillar-reference-for-yomi), but the `yomi-formula` package provides a set of examples that can be used to deploy MicroOS installations, Kubic, LVM, RAID or simple openSUSE Tumbleweed ones. In order to `salt-master` can find the pillar, we need to change the `pillar_roots` entry in the configuration file, or use the one provided by the package: ```bash cp -a /usr/share/yomi/pillar.conf /etc/salt/master.d/ systemctl restart salt-master.service ``` ## Enabling auto-sing in Yomi The images generated by the Open Build Service that are ready to be used together with Yomi contains a list a random UUID, that can be used as a auto-sing grain in `salt-master`. We can enable this feature adding the configuration file provided by the package: ```bash cp /usr/share/yomi/autosign.conf /etc/salt/master.d/ systemctl restart salt-master.service ``` ## Salt API in Yomi As described in the section [Salt API](#salt-api), we need to enable the `salt-api` service in order to provide a REST API service to `salt-minion`. This service is used by Yomi to monitor the installation, reading the event bus of Salt. To enable the real-time events we need to enable set `events` field to `yes` in the configuration section of the pillar. We can enable this service easily (after installing the `salt-api` package and the dependencies) using the provided configuration file: ```bash cp /usr/share/yomi/salt-api.conf /etc/salt/master.d/ systemctl restart salt-master.service ``` Feel free to edit `/etc/salt/master.d/salt-api.conf` and provide the required certificates to enable the SSL connection, an use a different authorization mechanism. The current one is based on reading the file `/usr/share/yomi/user-list.txt`, that is storing the password in plain text. So please, *do not* use this in production. ### Real time monitoring in Yomi Once we check that in our `config` of the pillar contains this: ```yaml config: events: yes ``` We can launch the `yomi-monitor` tool. ```bash export SALTAPI_URL=http://localhost:8000 export SALTAPI_EAUTH=file export SALTAPI_USER=salt export SALTAPI_PASS=linux yomi-monitor -r -y ``` The `yomi-monitor` tool store in a local cache the authentication tokens generated by Salt API. This will accelerate the next connection to the service, but sometimes can cause authentication errors (for example, when the cache is in place but the salt-master get reinstalled). The option `-r` makes sure that this cache is removed before connection. Check the help option of the tool for more information. # Booting a new machine As described in the previous sections, Yomi is a set of Salt states that are used to drive the installation of a new operating system. To take full control of the system where the installation will be done, you will need to boot from an external system that provides an already configured `salt-minion`, and a set of CLI tools required during the installation. We can deploy all the requirements using different mechanisms. One, for example, is via PXE boot. We can build a server that will deliver the Linux `kernel` and an `initrd` will all the required software. Another alternative is to have an already live ISO image that you use to boot from the USB port. There is an already available image that contains all the requirements in [Factory](https://build.opensuse.org/package/show/openSUSE:Factory/openSUSE-Tumbleweed-Yomi). This is an image build from openSUSE Tumbleweed repositories that includes a very minimal set of tools, including the openSUSE version of `salt-minion`. To use the last version of the image, together with the last version of `salt-minion` that includes all the patches that are under review in the SaltStack project, you can always use the version from the [devel project](https://build.opensuse.org/package/show/systemsmanagement:yomi/openSUSE-Tumbleweed-Yomi) Note that this image is a `_multibuild` one, and generates two different images. One is a LiveCD ISO image, ready to be booted from USB or DVD, and the other one is a PXE Boot ready image. ## The ISO image The ISO image is a LiveCD that can be booted from USB or from DVD, and the last version can be always be downloaded from: ```bash wget https://download.opensuse.org/repositories/systemsmanagement:/yomi/images/iso/openSUSE-Tumbleweed-Yomi.x86_64-livecd.iso ``` This image do not have root password, so if we have physical access to the node we can become root locally. The `sshd` service is enabled during boot time but for security reasons the user `root` cannot access via SSH (`PermitEmptyPasswords` is not set). To gain remote access to `root` we need to set the kernel command line parameter `ym.sshd=1` (for example, via PXE Boot). ## PXE Boot The second image available is a OEM ramdisk that can be booted from PXE Boot. To install the image we first need to download the file `openSUSE-Tumbleweed-Yomi.x86_64-${VERSION}-pxeboot-Build${RELEASE}.${BUILD}.install.tar` from the Factory, or directly from the development project. We need to start the `sftpd` service or use `dnsmasq` to behave also as a tftp server. There is some documentation in the [openSUSE wiki](https://en.opensuse.org/SDB:PXE_boot_installation), and if you are using QEMU you can also check the appendix document. ```bash mkdir -p /srv/tftpboot/pxelinux.cfg cp /usr/share/syslinux/pxelinux.0 /srv/tftpboot cd /srv/tftpboot tar -xvf $IMAGE cat <<EOF > /srv/tftpboot/pxelinux.cfg/default default yomi prompt 1 timeout 30 label yomi kernel pxeboot.kernel append initrd=pxeboot.initrd.xz rd.kiwi.install.pxe rd.kiwi.install.image=tftp://${SERVER}/openSUSE-Tumbleweed-Yomi.xz rd.kiwi.ramdisk ramdisk_size=1048576 EOF ``` This image is based on Tumbleweed, that leverage by default the predictable network interface name. If your image is based on a different one, be sure to add `net.ifnames=1` at the end of the `append` section. ## Finding the master node The `salt-minion` configuration in the Yomi image will search the `salt-master` system under the `salt` name. Is expected that the local DNS service will resolve the `salt` name to the correct IP address. During boot time of the Yomi image we can change the address where is expected to find the master node. To do that we can enter under the GRUB menu the entry `ym.master=my_master_address`. For example `ym.master=10.0.2.2` will make the minion to search the master in the address `10.0.2.2`. An internal systemd service in the image will detect this address and configure the `salt-minion` accordingly. Under the current Yomi states, this address will be copied under the new installed system, together with the key delivered by the `salt-master` service. This means that once the system is fully installed with the new operating system, the new `salt-minion` will find the master directly after the first boot. ## Setting the minion ID In a similar way, during the boot process we can set the minion ID that will be assigned to the `salt-minion`. Using the parameter `ym.minion_id`. For example, `ym.minion_id=worker01` will set the minion ID for this system as `worker01`. The rules for the minion ID are a bit more complicated. Salt, by default, set the minion ID equal to the FQDN or the IP of the node if no ID is specified. This cannot be a good idea if the IP changes, so the current rules are: * The value from `ym.minion_id` boot parameter. * The FQDN hostname of the system, if is different from localhost. * The MAC address of the first interface of the system. ## Adding user provided configuration Sometimes we need to inject in the `salt-minion` some extra configuration, before the service runs. For example, we might need to add some grains, or enable some feature in the `salt-minion` service running inside the image. To do that we have to options: we can pass an URL with the content, or we can add the full content as a parameter during the boot process. To pass an URL we should use `ym.config_url` parameter. For example, `ym.config_url=http://server.com/pub/myconfig.cfg` will download the configuration file, and will store it under the default name `config_url.cfg` in `/etc/salt/minion.d`. We can set a different name from the default via the parameter `ym.config_url_name`. In a similar way we can use the parameter `ym.config` to declare the full content of the user provided configuration file. You need to use quotes to mark the string and escaped control codes to indicate new lines or tabs, like `ym.config="grains:\n my_grain: my_value"`. This will create a file named `config.cfg`, and the name can be overwritten with the parameter `ym.config_name`. ## Container Because the versatility of Salt, is possible to execute the modules that belong to the `salt-minion` service Yomi without the requirement of any `salt-master` nor `salt-minion` service running. We could launch the installation via only the `salt-call` command in local mode. Because of that, es possible to deliver Yomi as a single container, composed of the different Salt and Yomi modules and states. We can boot a machine using any mechanism, like a recovery image, and use `podman` to register the Yomi container. This container will be executed as a privileged one, mapping the external devices inside the container space. To register the container we can do: ```bash podman pull registry.opensuse.org/systemsmanagement/yomi/images/opensuse/yomi:latest ``` Is recommended to create a local pillar directory; ```bash mkdir pillar ``` Once we have the pillar data, we can launch the installer: ```bash podman run --privileged --rm \ -v /dev:/dev \ -v /run/udev:/run/udev \ -v ./pillar:/srv/pillar \ <CONTAINER_ID> \ salt-call --local state.highstate ``` # Basic operations Once `salt-master` is configured and running, the `yomi-formula` states are available and a new system is booted with a up-to-date `salt-minion`, we can start to operate with Yomi. The usual process is simple: describe the pillar information and apply the `yomi` state to the node or nodes. Is not relevant how the pillar was designed (maybe using a smart template that cover all the cases or writing a raw YAML that only covers one single installation). In this section we will provide some hints about how get information and can help in this process. ## Getting hardware information The provided pillar are only an example of what we can do with Yomi. Eventually we need to adapt them based on the hardware that we have. We can discover the hardware configuration with different mechanism. One is get the `grains` information directly from the minion: ```bash salt node grains.items ``` We can get more detailed information using other Salt modules, like `partition.list`, `network.interfaces` or `udev.info`. With Yomi we provided a simple interface to `hwinfo` that provides in a single report some of the information that is required to make decisions about the pillar. ```bash # Synchronize all the modules to the minion salt node saltutil.sync_all # Get a short report about some devices salt node devices.hwinfo # Get a detailled report about some devices salt node devices.hwinfo short=no ``` ## Configuring the pillar The package `yomi-formula` provides some pillar examples that can be used as a reference when you are creating your own profiles. Salt search the pillar information in the directories listed in the `pillar_roots` configuration entry, and using the snippet from the section [Pillar in Yomi](#pillar-in-yomi), we can make those examples available in our system. In the case that we want to edit those files, we can copy them in a different directory and add it to the `pillar_roots` entry. ```bash mkdir -p /srv/pillar-yomi cp -a /usr/share/yomi/pillar/* /srv/pillar-yomi cat <<EOF > /etc/salt/master.d/pillar.conf pillar_roots: base: - /srv/pillar-yomi - /srv/pillar EOF systemctl restart salt-master.service ``` The pillar tree start with the `top.sls` file (there is another `top.sls` file for the states, do not confuse them). ```yaml base: '*': - installer ``` This file is used to map the node with the data that the states will use later. For this example the file that contain the data is `installer.sls`, but feel free to choose a different name when you are creating your own pillar. This `installer.sls` is used as an entry point for the rest of the data. Inside the file there is some Jinja templates that can be edited to define different kinds of installations. This feature is leveraged by the [openQA](https://github.com/os-autoinst/os-autoinst-distri-opensuse/tree/master/tests/yomi) tests, to easily make multiple deployments. You can edit the `{% set VAR=VAL %}` section to adjust it to your current profile, or create one from scratch. The files `_storage.sls.*` are included for different scenarios, and this is the place where the disk layout is described. Feel free to include it directly on your pillar, or use a different mechanism to decide the layout. ## Cleaning the disks Yomi try to be careful with the current data stored in the disks. By default will not remove any partition, nor will make an implicit decision about the device where the installation will run. If we want to remove the data from the device, we can use the provided `devices.wipe` execution module. ```bash # List the partitions salt node partition.list /dev/sda # Make sure that the new modules are in the minion salt node saltutil.sync_all # Remove all the partitions and the filesystem information salt node devices.wipe /dev/sda ``` To wipe all the devices defined in the pillar at once, we can apply the `yomi.storage.wipe` state. ```bash # Make sure that the new modules are in the minion salt node saltutil.sync_all # Remove all the partitions and the filesystem information salt node state.apply yomi.storage.wipe ``` ## Applying the yomi state Finally, to install the operating system defined by the pillar into the new node, we need to apply the high-state: ```bash salt node state.apply yomi ``` If we have a `top.sls` file similar to this example, living in `/srv/salt` or in any other place where `file_roots` option is configured: ```yaml base: '*': - yomi ``` We can apply directly the high state: ```bash salt node state.highstate ``` # Pillar reference for Yomi To install a new node, we need to provide some data to describe the installation requirements, like the layout of the partitions, file systems used, or what software to install inside the new deployment. This data is collected in what is Salt is known as a [pillar](https://docs.saltstack.com/en/latest/topics/tutorials/pillar.html). To configure the `salt-master` service to find the pillar, check the section [Looking for the pillar](#looking-for-the-pillar). Pillar can be associated with certain nodes in our network, making of this technique a basic one to map a description of how and what to install into a node. This mapping is done via the `top.sls` file: ```yaml base: 'C7:7E:55:62:83:17': - installer ``` In `installer.sls` we will describe in detail the installation parameters that will be applied to the node which minion-id match with `C7:7E:55:62:83:17`. Note that in this example we are using the MAC address of the first interface as a minion-id (check the section **Enabling Autosign** for an example). The `installer.sls` pillar consist on several sections, that we can describe here. ## `config` section The `config` section contains global configuration options that will affect the installer. * `events`: Boolean. Optional. Default: `yes` Yomi can fire Salt events before and after the execution of the internal states that Yomi use to drive the installation. Using the Salt API, WebSockets, or any other mechanism provided by Salt, we can listen the event bus and use this information to monitor the installer. Yomi provides a basic tool, `yomi-monitor`, that shows real time information about the installation process. To disable the events, set this parameter to `no`. Note that this option will add three new states for each single Yomi state. One extra state is executed always before the normal state, and is used to signalize that a new state will be executed. If the state is successfully terminated, a second extra state will send an event to signalize that the status of the state is positive. But if the state fails, a third state will send the fail signal. All those extra states will be showed in the final report of Salt. * `reboot`: String. Optional. Default: `yes` Control the way that the node will reboot. There are three possible values: * `yes`: Will produce a full reboot cycle. This value can be specified as the "yes" string, or the `True` boolean value. * `no`: Will no reboot after the installation. * `kexec`: Instead of rebooting, reload the new kernel installed in the node. * `halt`: The machine will halt at the end of the installation. * `shutdown`: The machine will shut down at the end of the installation. * `snapper`: Boolean. Optional. Default: `no` In Btrfs configurations (and in LVM, but still not implemented) we can install the snapper tool, to do automatic snapshots before and after updates in the system. One installed, a first snapshot will be done and the GRUB entry to boot from snapshots will be added. * `locale`: String. Optional. Default: `en_US.utf8` Sets the system locale, more specifically the LANG= and LC\_MESSAGES settings. The argument should be a valid locale identifier, such as `de_DE.UTF-8`. This controls the locale.conf configuration file. * `locale_message`: String. Optional. Sets the system locale, more specifically the LANG= and LC\_MESSAGES settings. The argument should be a valid locale identifier, such as `de_DE.UTF-8`. This controls the locale.conf configuration file. * `keymap`: String. Optional. Default: `us` Sets the system keyboard layout. The argument should be a valid keyboard map, such as `de-latin1`. This controls the "KEYMAP" entry in the vconsole.conf configuration file. * `timezone`: String. Optional. Default: `UTC` Sets the system time zone. The argument should be a valid time zone identifier, such as "Europe/Berlin". This controls the localtime symlink. * `hostname`: String. Optional. Sets the system hostname. The argument should be a host name, compatible with DNS. This controls the hostname configuration file. * `machine_id`: String. Optional. Sets the system's machine ID. This controls the machine-id file. If no one is provided, the one from the current system will be re-used. * `target`: String. Optional. Default: `multi-user.target` Set the default target used for the boot process. Example: ```yaml config: # Do not send events, useful for debugging events: no # Do not reboot after installation reboot: no # Always install snapper if possible snapper: yes # Set language to English / US locale: en_US.UTF-8 # Japanese keyboard keymap: jp # Universal Timezone timezone: UTC # Boot in graphical mode target: graphical.target ``` ## `partitions` section Yomi separate partitioning the devices from providing a file system, creating volumes or building arrays of disks. The advantage of this is that this, usually, compose better that other approaches, and makes more easy adding more options that needs to work correctly with the rest of the system. * `config`: Dictionary. Optional. Subsection that store some configuration options related with the partitioner. * `label`: String. Optional. Default: `msdos` Default label for the partitions of the devices. We use any `parted` partition recognized by `mklabel`, like `gpt`, `msdos` or `bsd`. For UEFI systems, we need to set it to `gpt`. This value will be used for all the devices if is not overwritten. * `initial_gap`: Integer. Optional. Default: `0` Initial gap (empty space) leaved before the first partition. Usually is recommended to be 1MB, so GRUB have room to write the code needed after the MBR, and the sectors are aligned for multiple SSD and hard disk devices. Also is relevant for the sector alignment in devices. The valid units are the same for `parted`. This value will be used for all the devices if is not overwritten. * `devices`: Dictionary. List of devices that will be partitioned. We can indicate already present devices, like `/dev/sda` or `/dev/hda`, but we can also indicate devices that will be present after the RAID configuration, like `/dev/md0` or `/dev/md/myraid`. We can use any valid device name in Linux such as all the `/dev/disk/by-id/...`, `/dev/disk/by-label/...`, `/dev/disk/by-uuid/...` and others. For each device we have: * `label`: String. Optional. Default: `msdos` Partition label for the device. The meaning and the possible values are identical for `label` in the `config` section. * `initial_gap`: Integer. Optional. Default: `0` Initial gap (empty space) leave before the first partition for this device. * `partitions`: Array. Optional. Partitions inside a device are described with an array. Each element of the array is a dictionary that describe a single partition. * `number`: Integer. Optional. Default: `loop.index` Expected partition number. Eventually this parameter will be really optional, when the partitioner can deduce it from other parameters. Today is better to be explicit in the partition number, as this will guarantee that the partition is found in the hard disk if present. If is not set, number will be the current index position in the array. * `id`: String. Optional. Full name of the partition. For example, valid ids can be `/dev/sda1`, `/dev/md0p1`, etc. Is optional, as the name can be deduced from `number`. * `size`: Float or String. Size of the partition expressed in `parted` units. All the units needs to match for partitions on the same device. For example, if `initial_gap` or the first partition is expressed in MB, all the sized needs to be expressed in MB too. The last partition can use the string `rest` to indicate that this partition will use all the free space available. If after this another partition is defined, Yomi will show a validation error. * `type`: String. A string that indicate for what this partition will be used. Yomi recognize several types: * `swap`: This partition will be used for SWAP. * `linux`: Partition used to root, home or any data. * `boot`: Small partition used for GRUB when in BIOS and `gpt`. * `efi`: EFI partition used by GRUB when UEFI. * `lvm`: Partition used to build an LVM physical volume. * `raid`: Partition that will be a component of an array. Example: ```yaml partitions: config: label: gpt initial_gap: 1MB devices: /dev/sda: partitions: - number: 1 size: 256MB type: efi - number: 2 size: 1024MB type: swap - number: 3 size: rest type: linux ``` ## `lvm` section To build an LVM we usually create some partitions (in the `partitions` section) with the `lvm` type set, and in the `lvm` section we describe the details. This section is a dictionary, were each key is the name of the LVM volume, and inside it we can find: * `devices`: Array. List of components (partitions or full devices) that will constitute the physical volumes and the virtual group of the LVM. If the element of the array is a string, this will be the name of a device (or partition) that belongs to the physical group. If the element is a dictionary it will contains: * `name`: String. Name of the device or partition. The rest of the elements of the dictionary will be passed to the `pvcreate` command. Note that the name of the virtual group will be the key where this definition is under. * `volumes`: Array. Each element of the array will define: * `name`: String. Name of the logical volume under the volume group. The rest of the elements of the dictionary will be passed to the `lvcreate` command. For example, `size` and `extents` are used to indicate the size of the volume, and they can include a suffix to indicate the units. Those units will be the same used for `lvcreate`. The rest of the elements of this section will be passed to the `vgcreate` command. Example: ```yaml lvm: system: devices: - /dev/sda1 - /dev/sdb1 - name: /dev/sdc1 dataalignmentoffset: 7s clustered: 'n' volumes: - name: swap size: 1024M - name: root size: 16384M - name: home extents: 100%FREE ``` ## `raid` section In the same way that LVM, to create RAID arrays we can setup first partitions (with the type `raid`) and configure the details in this section. Also, similar to the LVM section, the keys a correspond to the name of the device where the RAID will be created. Valid values are like `/dev/md0` or `/dev/md/system`. * `level`: String. RAID level. Valid values can be `linear`, `raid0`, `0`, `stripe`, `raid1`, `1`, `mirror`, `raid4`, `4`, `raid5`, `5`, `raid6`, `6`, `raid10`, `10`, `multipath`, `mp`, `faulty`, `container`. * `devices`: Array. List of devices or partitions that build the array. * `metadata`: String. Optional. Default: `default` Metadata version for the superblock. Valid values are `0`, `0.9`, `1`, `1.0`, `1.1`, `1.2`, `default`, `ddm`, `imsm`. The user can specify more parameters that will be passed directly to `mdadm`, like `spare-devices` to indicate the number of extra devices in the initial array, or `chunk` to speficy the chunk size. Example: ```yaml raid: /dev/md0: level: 1 devices: - /dev/sda1 - /dev/sdb1 - /dev/sdc1 spare-devices: 1 metadata: 1.0 ``` ## `filesystems` section The partitions, devices or arrays created in previous sections usually requires a file system. This section will simply list the device name and the file system (and properties) that will be applied to it. * `filesystem`. String. File system to apply in the device. Valid values are `swap`, `linux-swap`, `bfs`, `btrfs`, `xfs`, `cramfs`, `ext2`, `ext3`, `ext4`, `minix`, `msdos`, `vfat`. Technically Salt will search for a command that match `mkfs.<filesystem>`, so the valid options can be more extensive that the one listed here. * `mountpoint`. String. Mount point where the device will be registered in `fstab`. * `fat`. Integer. Optional. If the file system is `vfat` we can force the FAT size, like 12, 16 or 32. * `subvolumes`. Dictionary. For `btrfs` file systems we can specify more details. * `prefix`. String. Optional. `btrfs` sub-volume name where the rest of the sub-volumes will be under. For example, if we set `prefix` as `@` and we create a sub-volume named `var`, Yomi will create it as `@/var`. * `subvolume`. Dictionary. * `path`. String. Path name for the sub-volume. * `copy_on_write`. Boolean. Optional. Default: `yes` Value for the copy-on-write option in `btrfs`. Example: ```yaml filesystems: /dev/sda1: filesystem: vfat mountpoint: /boot/efi fat: 32 /dev/sda2: filesystem: swap /dev/sda3: filesystem: btrfs mountpoint: / subvolumes: prefix: '@' subvolume: - path: home - path: opt - path: root - path: srv - path: tmp - path: usr/local - path: var copy_on_write: no - path: boot/grub2/i386-pc - path: boot/grub2/x86_64-efi ``` ## `bootloader` section * `device`: String. Device name where GRUB2 will be installed. Yomi will take care of detecting if is a BIOS or an UEFI setup, and also if Secure-Boot in activated, to install and configure the bootloader (or the shim loader) * `timeout`: Integer. Optional. Default: `8` Value for the `GRUB_TIMEOUT` parameter. * `kernel`: String. Optional. Default: `splash=silent quiet` Line assigned to the `GRUB_CMDLINE_LINUX_DEFAULT` parameter. * `terminal`: String. Optional. Default: `gfxterm` Value for the `GRUB_TERMINAL` parameter. If the value is set to `serial`, we need to add content to the `serial_command` parameter. If the value is set to `console`, we can pass the console parameters to the `kernel` parameter. For example, `kernel: splash=silent quiet console=tty0 console=ttyS0,115200` * `serial_command`: String. Optional Value for the `GRUB_SERIAL_COMMAND` parameter. If there is a value, `GRUB_TERMINAL` is expected to be `serial`. * `gfxmode`: String. Optional. Default: `auto` Value for the `GRUB_GFXMODE` parameter. * `theme`: Boolean. Optional. Default: `no` If `yes` the `grub2-branding` package will be installed and configured. * `disable_os_prober`: Boolean. Optional. Default: `False` Value for the `GRUB_DISABLE_OS_PROBER` parameter. Example: ```yaml bootloader: device: /dev/sda ``` ## `software` section We can indicate the repositories that will be registered in the new installation, and the packages and patterns that will be installed. * `config`. Dictionary. Optional Local configuration for the software section. Except `minimal`, `transfer`, and `verify` all the options can be overwritten in each repository definition. * `minimal`: Boolean. Optional. Default: `no` Configure zypper to make a minimal installation, excluding recommended, documentation and multi-version packages. * `transfer`: Boolean. Optional. Default: `no` Transfer the current repositories (maybe defined in the media installation) into the installed system. If marked, this step will be done early, so any future action could update or replace one of the repositories. * `verify`: Boolean. Optional. Default: `yes` Verify the package key when installing. * `enabled`: Boolean. Optional. Default: `yes` If the repository is enabled, packages can be installed from there. A disabled repository will not be removed. * `refresh`: Boolean. Optional. Default: `yes` Enable auto-refresh of the repository. * `gpgcheck`: Boolean. Optional. Default: `yes` Enable or disable the GPG check for the repositories. * `gpgautoimport`: Boolean. Optional. Default: `yes` If enabled, automatically trust and import public GPG key for the repository. * `cache`: Boolean. Optional. Default: `no` If the cache is enabled, will keep the RPM packages. * `repositories`. Dictionary. Optional Each key of the dictionary will be the alias under where this repository is registered, and the key, if is a string, the URL associated with it. If the key is an dictionary, we can overwrite some of the default configuration options set in the `config` section, with the exception of `minimal`. There are some more elements that we can set for the repository: * `url`: String. URL of the repository. * `name`: String. Optional Descriptive name for the repository. * `priority`: Integer. Optional. Default: `0` Set priority of the repository. * `packages`. Array. Optional List of packages or patters to be installed. * `image`. Dictionary. Optional We can bootstrap the root file system based on a partition image generate by KIWI (or any other mechanism), that will be copied into the partition that have the root mount point assigned. This can be used to speed the installation process. Those images needs to contain only the file system and the data. If the image contains a boot loader or partition information, the image will fail during the resize operation. To validate if the image is suitable, a simple `file image.raw` will do. * `url`: String. URL of the image. As internally we are using curl to fetch the image, we can support multiple protocols like `http://`, `https://` or `tftp://` among others. The image can be compressed, and in that case one of those extensions must to be used to indicate the format: [`gz`, `bz2`, `xz`] * `md5`|`sha1`|`sha224`|`sha256`|`sha384`|`sha512`: String. Optional Checksum type and value used to validate the image. If this field is present but empty (only the checksum type, but with no value attached), the state will try to fetch the checksum fail from the same URL given in the previous field. If the path contains an extension for a compression format, this will be replaced with the checksum type as a new extension. For example, if the URL is `http://example.com/image.xz`, the checksum type is `md5`, and no value is provided, the checksum will be expected at `http://example.com/image.md5`. But if the URL is something like `http://example.com/image.ext4`, the checksum will be expected in the URL `http://example.com/image.ext4.md5`. If the checksum type is provided, the value for the last image will be stored in the Salt cache, and will be used to decide if the image in the URL is different from the one already copied in the partition. If this is the case, no image will be downloaded. Otherwise a new image will be copied, and the old one will be overwritten in the same partition. Example: ```yaml software: repositories: repo-oss: "http://download.opensuse.org/tumbleweed/repo/oss" update: url: http://download.opensuse.org/update/tumbleweed/ name: openSUSE Update packages: - patterns-base-base - kernel-default ``` ## `suseconnect` section Very related with the previous section (`software`), we can register an SLE product and modules using the `SUSEConnect` command. In order to `SUSEConnect` to succeed, a product needs to be present already in the system. This imply that the register must happen after (at least a partial) installation has been done. As `SUSEConnect` will register new repositories, this also imply that not all the packages that can be enumerated in the `software` section can be installed. To resolve both conflicts, Yomi will first install the packages listed in the `sofwtare` section, and after the registration, the packages listed in this `suseconnect` section. * `config`. Dictionary. Local configuration for the section. It is not optional as there is at least one parameter that is required for any registration. * `regcode`. String. Subscription registration code for the product to be registered. * `email`. String. Optional. Email address for product registration. * `url`. String. Optional. URL of registration server (e.g. https://scc.suse.com) * `version`. String. Optional. Version part of the product name. If the product name do not have a version, this default value will be used. * `arch`. String. Optional. Architecture part of the product name. If the product name do not have an architecture, this default value will be used. * `products`. Array. Optional. Product names to register. The expected format is <name>/<version>/<architecture>. If only <name> is used, the values for <version> and <architecture> will be taken from the `config` section. If the product / module have a different registration code than the one declared in the `config` sub-section, we can declare a new one via a dictionary. * `name`. String. Optional. Product names to register. The expected format is <name>/<version>/<architecture>. If only <name> is used, the values for <version> and <architecture> will be taken from the `config` section. * `regcode`. String. Optional. Subscription registration code for the product to be registered. * `packages`. Array. Optional List of packages or patters to be installed from the different modules. Example: ```yaml suseconnect: config: regcode: SECRET-CODE products: - sle-module-basesystem/15.2/x86_64 - sle-module-server-applications/15.2/x86_64 - name: sle-module-live-patching/15.2/x86_64 regcode: SECRET-CODE ``` ## `salt-minion` section Install and configure the salt-minion service. * `config`. Boolean. Optional. Default: `no` If `yes`, the configuration and cetificates of the new minion will be the same that the current minion that is activated. This will copy the minion configuration, certificates and grains, together with the cached modules and states that are usually synchronized before a highstate. This option will be replaced in the future with more detailed ones. Example: ```yaml salt-minion: config: yes ``` ## `services` section We can list the services that will be enabled or disabled during boot time. * `enabled`. Array. Optional List of services that will be enabled and started during the boot. * `disabled`. Array. Optional List of services that will be exclicitly disabled during the boot. Example: ```yaml services: enabled: - salt-minion ``` ## `networks` section We can list the networks available in the target system. If the list is not provided, Yomi will try to deduce the network configuration based on the current setup. * `interface`. String. Name of the interface. Example: ```yaml networks: - interface: ens3 ``` ## `users` section In this section we can list a simple list of users and passwords that we expect to find once the system is booted. * `username`. String. Login or username for the user. * `password`. String. Optional. Shadow password hash for the user. * `certificates`. Array. Optional. Certificates that will be added to .ssh/authorized_keys. Use only the encoded key (remove the "ssh-rsa" prefix and the "user@host" suffix). Example: ```yaml users: - username: root password: "$1$wYJUgpM5$RXMMeASDc035eX.NbYWFl0" - username: aplanas certificates: - "AAAAB3NzaC1yc2EAAAADAQABAAABAQDdP6oez825gnOLVZu70KqJXpqL4fGf\ aFNk87GSk3xLRjixGtr013+hcN03ZRKU0/2S7J0T/dICc2dhG9xAqa/A31Qac\ hQeg2RhPxM2SL+wgzx0geDmf6XDhhe8reos5jgzw6Pq59gyWfurlZaMEZAoOY\ kfNb5OG4vQQN8Z7hldx+DBANPbylApurVz6h5vvRrkPfuRVN5ZxOkI+LeWhpo\ vX5XK3eTjetAwWEro6AAXpGoQQQDjSOoYHCUmXzcZkmIWEubCZvAI4RZ+XCZs\ +wTeO2RIRsunqP8J+XW4cZ28RZBc9K4I1BV8C6wBxN328LRQcilzw+Me+Lfre\ eDPglqx" ``` 07070100000003000081ED0000000000000000000000016130D1CF00005D24000000000000000000000000000000000000003000000000yomi-0.0.1+git.1630589391.4557cfd/autoyast2yomi#!/usr/bin/python3 # -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import argparse import crypt import json import logging from pathlib import Path import xml.etree.ElementTree as ET class PathDict(dict): def path(self, path, default=None): result = self for item in path.split("."): if item in result: result = result[item] else: return default return result class Convert: def __init__(self, control): """Construct a Convert class from a control XML object""" self.control = control # Store the parsed version of the control file self._control = None self.pillar = {} @staticmethod def _find(element, name): """Find a single element in a XML tree""" return element.find("{{http://www.suse.com/1.0/yast2ns}}{}".format(name)) @staticmethod def _get_tag(element): """Get element name, without namespace""" return element.tag.replace("{http://www.suse.com/1.0/yast2ns}", "") @staticmethod def _get_type(element): """Get element type if any""" return element.attrib.get("{http://www.suse.com/1.0/configns}type") @staticmethod def _get_text(element): """Get element text if any""" if element is not None and element.text is not None: return element.text.strip() @staticmethod def _get_bool(element): """Get element boolean value if any""" _text = Convert._get_text(element) if _text: return _text.lower() == "true" @staticmethod def _get_int(element): """Get element integer value if any""" _text = Convert._get_text(element) if _text: try: return int(_text) except ValueError: pass @staticmethod def _get(element): """Recursively parse the XML tree""" type_ = Convert._get_type(element) if not type_ and not len(element): return Convert._get_text(element) elif type_ == "symbol": return Convert._get_text(element) elif type_ == "boolean": return Convert._get_bool(element) elif type_ == "integer": return Convert._get_int(element) elif type_ == "list": return [Convert._get(subelement) for subelement in element] elif not type_ and len(element): return { Convert._get_tag(subelement): Convert._get(subelement) for subelement in element } else: logging.error("Element type not recognized: %s", type_) @staticmethod def _parse(element): """Parse the XML tree entry point""" return PathDict({Convert._get_tag(element): Convert._get(element)}) def convert(self): """Transform a XML control file into a Yomi pillar""" self._control = Convert._parse(self.control.getroot()) self._convert_config() self._convert_partitions() self._convert_lvm() self._convert_raid() self._convert_filesystems() self._convert_bootloader() self._convert_software() self._convert_suseconnect() self._convert_salt_minion() self._convert_services() self._convert_users() return self.pillar def _reboot(self): """Detect if a reboot is required""" reboot = False mode = self._control.path("profile.general.mode", {}) if mode.get("final_halt") or mode.get("halt"): reboot = "shutdown" elif mode.get("final_reboot") or mode.get("forceboot"): reboot = True return reboot def _snapper(self): """Detect if snapper is required""" partitioning = self._control.path("profile.partitioning", PathDict()) snapper = any( drive.get("enable_snapshots", True) for drive in partitioning if any( partition for partition in drive.get("partitions", []) if partition.get("filesystem", "btrfs") == "btrfs" ) ) snapper |= self._control.path("profile.bootloader.suse_btrfs", False) return snapper def _keymap(self): """Translate keymap configuration""" keymap = self._control.path("profile.keyboard.keymap", "english-us") return { "english-us": "us", "english-uk": "gb", "german": "de-nodeadkeys", "german-deadkey": "de", "german-ch": "ch", "french": "fr", "french-ch": "ch-fr", "french-ca": "ca", "cn-latin1": "ca-multix", "spanish": "es", "spanish-lat": "latam", "spanish-lat-cp850": "es", "spanish-ast": "es-ast", "italian": "it", "persian": "ir", "portugese": "pt", "portugese-br": "br", "portugese-br-usa": "us-intl", "greek": "gr", "dutch": "nl", "danish": "dk", "norwegian": "no", "swedish": "se", "finnish": "fi-kotoistus", "czech": "cz", "czech-qwerty": "cz-qwerty", "slovak": "sk", "slovak-qwerty": "sk-qwerty", "slovene": "si", "hungarian": "hu", "polish": "pl", "russian": "ruwin_alt-UTF-8", "serbian": "sr-cy", "estonian": "ee", "lithuanian": "lt", "turkish": "tr", "croatian": "hr", "japanese": "jp", "belgian": "be", "dvorak": "us-dvorak", "icelandic": "is", "ukrainian": "ua-utf", "khmer": "khmer", "korean": "kr", "arabic": "arabic", "tajik": "tj_alt-UTF8", "taiwanese": "us", "chinese": "us", "romanian": "ro", "us-int": "us-intl", }.get(keymap, "us") def _convert_config(self): """Convert the config section of a pillar""" config = self.pillar.setdefault("config", {}) # Missing fields: # * locale_message # * machine_id config["events"] = True config["reboot"] = self._reboot() config["snapper"] = self._snapper() config["locale"] = self._control.path("profile.language.language", "en_US.utf8") config["keymap"] = self._keymap() config["timezone"] = self._control.path("profile.timezone.timezone", "UTC") hostname = self._control.path("profile.networking.dns.hostname") if hostname: config["hostname"] = hostname config["target"] = self._control.path( "profile.services-manager.default_target", "multi-user.target" ) def _size(self, partition): """Detect the size of a partition""" size = partition.get("size") return "rest" if size == "max" or not size else size def _type(self, partition): """Detect the type of a partition""" partition_id = partition.get("partition_id") if not partition_id: filesystem = partition.get("filesystem") if filesystem or partition.get("mount"): partition_id = 130 if filesystem == "swap" else 131 elif partition.get("lvm_group"): partition_id = 142 elif partition.get("raid_name"): partition_id = 253 else: # 'boot' type if is not a file system, LVM, nor RAID return "boot" return {130: "swap", 131: "linux", 142: "lvm", 253: "raid", 259: "efi"}[ partition_id ] def _label(self, drive): """Detect the kind of partition table of a device""" disklabel = drive.get("disklabel", "gpt") if disklabel and disklabel != "none" and not drive.get("raid_options"): return disklabel def _convert_partitions(self): """Convert the partitions section of a pillar""" partitions = self.pillar.setdefault("partitions", {}) for drive in self._control.path("profile.partitioning", []): # If is part of a logical volume, we skip the drive if drive.get("is_lvm_vg"): continue # If the device is missing, we cannot build the pillar device = drive.get("device") if not device: logging.error("Device missing in partitioning") continue devices = partitions.setdefault("devices", {}) _device = devices.setdefault(device, {}) label = self._label(drive) if label: _device["label"] = label _partitions = _device.setdefault("partitions", []) for index, partition in enumerate(drive.get("partitions", [])): _partition = {} _partition["number"] = partition.get("partition_nr", index + 1) _partition["size"] = self._size(partition) _partition["type"] = self._type(partition) if _partition: _partitions.append(_partition) def _convert_lvm(self): """Convert the lvm section of a pillar""" lvm = {} for drive in self._control.path("profile.partitioning", []): # If the device is missing, we cannot build the pillar device = drive.get("device") if not device: logging.error("Device missing in partitioning") continue if drive.get("is_lvm_vg"): lvm_group = Path(device).name group = lvm.setdefault(lvm_group, {}) volumes = group.setdefault("volumes", []) for partition in drive.get("partitions", []): volumes.append( {"name": partition["lv_name"], "size": partition["size"]} ) # Group parameters pesize = drive.get("pesize") if pesize: group["physicalextentsize"] = pesize else: for index, partition in enumerate(drive.get("partitions", [])): lvm_group = partition.get("lvm_group") if lvm_group: partition_nr = partition.get("partition_nr", index + 1) group = lvm.setdefault(lvm_group, {}) devices = group.setdefault("devices", []) devices.append("{}{}".format(device, partition_nr)) if lvm: self.pillar["lvm"] = lvm def _convert_raid(self): """Convert the raid section of a pillar""" raid = {} for drive in self._control.path("profile.partitioning", []): # If the device is missing, we cannot build the pillar device = drive.get("device") if not device: logging.error("Device missing in partitioning") continue raid_options = drive.get("raid_options") if raid_options: _device = raid.setdefault(device, {}) chunk_size = raid_options.get("chunk_size") if chunk_size: _device["chunk"] = chunk_size parity_algorithm = raid_options.get("parity_algorithm") if parity_algorithm: _device["parity"] = parity_algorithm.replace("_", "-") _device["level"] = raid_options.get("raid_type", "raid1") device_order = raid_options.get("device_order") if device_order: _device["devices"] = device_order continue for index, partition in enumerate(drive.get("partitions", [])): raid_name = partition.get("raid_name") if raid_name: partition_nr = partition.get("partition_nr", index + 1) _device = raid.setdefault(raid_name, {}) devices = _device.setdefault("devices", []) devices.append("{}{}".format(device, partition_nr)) if raid: self.pillar["raid"] = raid def _convert_filesystems(self): filesystems = self.pillar.setdefault("filesystems", {}) for drive in self._control.path("profile.partitioning", []): # If the device is missing, we cannot build the pillar device = drive.get("device") if not device: logging.error("Device missing in partitioning") continue for index, partition in enumerate(drive.get("partitions", [])): filesystem = {} if drive.get("is_lvm_vg"): lv_name = partition["lv_name"] _partition = str(Path(device, lv_name)) elif drive.get("raid_options"): partition_nr = partition.get("partition_nr", index + 1) _partition = "{}p{}".format(device, partition_nr) else: partition_nr = partition.get("partition_nr", index + 1) _partition = "{}{}".format(device, partition_nr) _filesystem = partition.get("filesystem") if _filesystem: filesystem["filesystem"] = _filesystem mount = partition.get("mount") if mount: filesystem["mountpoint"] = mount subvolumes = partition.get("subvolumes") if _filesystem == "btrfs" and subvolumes: _subvolumes = filesystem.setdefault("subvolumes", {}) _subvolumes["prefix"] = partition.get("subvolumes_prefix", "@") subvolume = _subvolumes.setdefault("subvolume", []) for _subvolume in subvolumes: if isinstance(_subvolume, str): subvolume.append({"path": _subvolume}) else: subvolume.append(_subvolume) if _partition and filesystem: filesystems[_partition] = filesystem def _kernel(self, bootloader_global): append = bootloader_global.get("append", "") cpu_mitigations = bootloader_global.get("cpu_mitigations", "") if cpu_mitigations: cpu_mitigations = ( "noibrs noibpb nopti nospectre_v2 nospectre_v1 " "l1tf=off nospec_store_bypass_disable " "no_stf_barrier mds=off mitigations=off" ) else: cpu_mitigations = "" vgamode = bootloader_global.get("vgamode", "") if vgamode and vgamode not in append: vgamode = "vga={}".format(vgamode) else: vgamode = "" return " ".join( param for param in ("splash=silent quiet", append, cpu_mitigations, vgamode) if param ) def _convert_bootloader(self): """Convert the bootloader section of the pillar""" bootloader = self.pillar.setdefault("bootloader", {}) _global = self._control.path("profile.bootloader.global", {}) # TODO: If EFI, we will be sure to create a EFI partition # TODO: `boot_custom` is not used to store the device device = _global.get("boot_custom") if device: bootloader["device"] = device else: logging.error("Bootloader device not found in control file") timeout = _global.get("timeout") if timeout: bootloader["timeout"] = timeout bootloader["kernel"] = self._kernel(_global) terminal = _global.get("terminal") if terminal: bootloader["terminal"] = terminal serial = _global.get("serial") if serial: bootloader["serial_command"] = serial gfxmode = _global.get("gfxmode") if gfxmode: bootloader["gfxmode"] = gfxmode bootloader["theme"] = True os_prober = _global.get("os_prober") if os_prober is not None: bootloader["disable_os_prober"] = not os_prober def _repositories(self, add_on): return { entry["alias"]: entry["media_url"] for add_on_type in ("add_on_products", "add_on_others") for entry in add_on.get(add_on_type, []) } def _packages(self, software, include_pre, include_post): packages = [] if include_pre: for product in software.get("products", []): packages.append("product:{}".format(product)) if include_pre: for pattern in software.get("patterns", []): packages.append("pattern:{}".format(pattern)) if include_post: for pattern in software.get("post-patterns", []): packages.append("pattern:{}".format(pattern)) if include_pre: for package in software.get("packages", []): packages.append(package) if include_post: for package in software.get("post-packages", []): packages.append(package) kernel = software.get("kernel") if include_pre and kernel: packages.append(kernel) return packages def _convert_software(self): """Convert the software section of the pillar""" software = self.pillar.setdefault("software", {}) _software = self._control.path("profile.software", {}) install_recommended = _software.get("install_recommended") if install_recommended is not None: config = software.setdefault("config", {}) config["minimal"] = not install_recommended add_on = self._control.path("profile.add-on", {}) if not add_on: logging.error("No repositories will be registered") software["repositories"] = self._repositories(add_on) software["packages"] = self._packages( _software, include_pre=True, include_post="suse_register" not in self._control["profile"], ) def _products(self, suse_register): products = [] for addon in suse_register.get("addons", []): products.append("/".join((addon["name"], addon["version"], addon["arch"]))) return products def _convert_suseconnect(self): """Convert the suseconnect section of the pillar""" suseconnect = self.pillar.get("suseconnect", {}) suse_register = self._control.path("profile.suse_register", {}) if not suse_register: return config = suseconnect.setdefault("config", {}) reg_code = suse_register.get("reg_code") if reg_code: config["regcode"] = reg_code email = suse_register.get("email") if email: config["email"] = email reg_server = suse_register.get("reg_server") if reg_server: config["url"] = reg_server suseconnect["products"] = self._products(suse_register) software = self._control.path("profile.software", {}) packages = self._packages(software, include_pre=False, include_post=True) if packages: suseconnect["packages"] = packages if suseconnect: self.pillar["suseconnect"] = suseconnect def _convert_salt_minion(self): """Convert the salt-minion section of the pillar""" self.pillar.setdefault("salt-minion", {"configure": True}) def _services(self, services): _services = [] for service in services: if not service.endswith((".service", ".socket", ".timer")): service = "{}.service".format(service) _services.append(service) return _services def _convert_services(self): """Convert the services section of the pillar""" services = self.pillar.get("services", {}) enable = self._control.path("profile.services-manager.services.enable", []) for service in self._services(enable): services.setdefault("enabled", []).append(service) disable = self._control.path("profile.services-manager.services.disable", []) for service in self._services(disable): services.setdefault("disabled", []).append(service) on_demand = self._control.path( "profile.services-manager.services.on_demand", [] ) for service in self._services(on_demand): services.setdefault("enabled", []).append( service.replace(".service", ".socket") ) services.setdefault("disabled", []).append(service) if services: self.pillar["services"] = services @staticmethod def _password(user, salt=None): password = user.get("user_password") if password and not user.get("encrypted"): salt = salt if salt else crypt.mksalt(crypt.METHOD_MD5) password = crypt.crypt(password, salt) return password def _certificates(self, user): certificates = [] for certificate in user.get("authorized_keys", []): parts = certificate.split() for index, part in enumerate(parts): if part in ("ssh-rsa", "ssh-dss", "ssh-ed25519") or part.startswith( "ecdsa-sha" ): certificates.append(parts[index + 1]) break return certificates def _convert_users(self): """Convert the users section of the pillar""" users = self.pillar.get("users", []) # TODO parse the fullname, uid, gid, etc. fields _users = self._control.path("profile.users", []) for _user in _users: user = {"username": _user["username"]} password = Convert._password(_user) if password: user["password"] = password certificates = self._certificates(_user) if certificates: user["certificates"] = certificates users.append(user) if users: self.pillar["users"] = users if __name__ == "__main__": parser = argparse.ArgumentParser(description="Convert AutoYaST control files") parser.add_argument("control", metavar="CONTROL.XML", help="autoyast control file") parser.add_argument( "-o", "--out", default="yomi.json", help="output file (default: yomi.json)" ) args = parser.parse_args() control = ET.parse(args.control) convert = Convert(control) pillar = convert.convert() with open(args.out, "w") as f: json.dump(pillar, f, indent=4) 07070100000004000041ED0000000000000000000000036130D1CF00000000000000000000000000000000000000000000002700000000yomi-0.0.1+git.1630589391.4557cfd/docs07070100000005000081A40000000000000000000000016130D1CF000018CB000000000000000000000000000000000000004300000000yomi-0.0.1+git.1630589391.4557cfd/docs/appendix-how-to-use-qemu.md# Appendix: How to use QEMU to test Yomi We can use libvirt, VirtualBox or real hardware to test Yomi. In this appendix we will give the basic instructions to setup QEMU with KVM to create a local network that will enable the communication of the nodes between each other, and with the guest. ## General overview We will use `qemu-system-x86_64` and the OVMF firmware to deploy UEFI nodes, and `socat` and `dnsmasq` to build a local network where our nodes can communicate. With QEMU we usually need to create some bridges and tun/tap interfaces that enable the communication between the local instances. To provide external access to those instances, we also usually need to enable the masquerading via `iptables`, and `ip_forward` via `sysctl` in out host. But using `socat` and `dnsmasq` we can avoid this. For this to work we will need two interfaces in the virtual machine. One will be owned by QEMU, that will use the user networking (SLIRP) back-end. In this network mode, the interface will have always the IP 10.0.2.15 in the VM side, and the host is reachable via the 10.0.2.2 IP. There is also an internal DNS under the IP 10.0.2.3, that is managed by QEMU and cannot be configured. SLIRP is optional and more complicated QEMU deployments disable this back-end by default. But for us is an easy way to have a connection between the VM and the host. If we maintain SLIPR operational, all the VMs will have the same IP, and all of them will see the host machine via the same IP too, but they cannot see each other. To resolve this we can add a second virtual interface in the VM, that using multi-cast, will be used as a communication channel between the VMs. We will need to use to external tools to enable this multi-cast communication. One, `socat`, will create a new virtual interface named `vmlan` in the host, where all the VMs will be connected to. And the other is `dnsmasq`, that will be used as a local DHCP / DNS server that will work on this new interface. ### Creating the local network First we will need to install both tools: ```bash zypper in socat dnsmasq ``` Now we need to use `socat` to create a new virtual interface named `vmlan`, that will expose the IP 10.0.3.1 to the host. At the other side we will have the multicast socket from QEMU. ```bash sudo socat \ UDP4-DATAGRAM:230.0.0.1:1234,sourceport=1234,reuseaddr,ip-add-membership=230.0.0.1:127.0.0.1 \ TUN:10.0.3.1/24,tun-type=tap,iff-no-pi,iff-up,tun-name=vmlan ``` If you see the error `Network is unreachable`, check if all the interfaces have an IP assigned (this can be the case when running inside a VM). But if the error message is `Device or resource busy`, check that there is not a previous `socat` process running for the same connection. Move this process in a second plane, and check that via `ip a s` we have the `vmlan` interface. We will now attach a DHCP / DNS server to this new interface, so the new nodes will have a predicted IP and hostname. Also the nodes will find the master using a name that can be resolved. ```bash sudo dnsmasq --no-daemon \ --interface=vmlan \ --except-interface=lo \ --except-interface=em1 \ --bind-interfaces \ --dhcp-range=10.0.3.100,10.0.3.200 \ --dhcp-option=option:router,10.0.3.101 \ --dhcp-host=00:00:00:11:11:11,10.0.3.101,master \ --dhcp-host=00:00:00:22:22:22,10.0.3.102,worker1 \ --dhcp-host=00:00:00:33:33:33,10.0.3.103,worker2 \ --host-record=master,10.0.3.101 ``` This command will deliver IPs into the interface `vmlan` from the range 10.0.3.100 to 10.0.3.200. The service will ignore the petitions from the local host and the `em1` interface. If your interfaces are named differently, you will need to adjust the command accordingly. The hostnames `master`, `worker1` and `worker2` will be assigned based on the MAC address, and `master` name will be always resolved to 10.0.3.101. This will simplify the configuration of the salt-minion later. ### Connecting QEMU to the new network We can now launch QEMU to have two interfaces. One will be connected to the new `vmlan` network, via the multi-cast socket option, and the other interface will be connected to the host machine. Because we will use UEFI, we will need first to copy the OVMF firmware locally. ```bash cp /usr/share/qemu/ovmf-x86_64-code.bin . cp /usr/share/qemu/ovmf-x86_64-vars.bin . ``` Now we can launch QEMU: ```bash # Local copy for the variable OVMF file cp -af ovmf-x86_64-vars.bin ovmf-x86_64-vars-node.bin # Create the file that will be used as a hard-disk qemu-img create -f qcow2 hda-node.qcow2 50G qemu-system-x86_64 -m 2048 -enable-kvm \ -netdev socket,id=vmlan,mcast=230.0.0.1:1234 \ -device virtio-net-pci,netdev=vmlan,mac=00:00:00:11:11:11 \ -netdev user,id=net0,hostfwd=tcp::10022-:22 \ -device virtio-net-pci,netdev=net0,mac=10:00:00:11:11:11 \ -cdrom *.iso \ -hda hda-node.qcow2 \ -drive if=pflash,format=raw,unit=0,readonly,file=./ovmf-x86_64-code.bin \ -drive if=pflash,format=raw,unit=1,file=./ovmf-x86_64-vars-node.bin \ -smp 2 \ -boot d & ``` The first interface will be connected to the `vmlan` via a multi-cast socket. The second interface will be the SLIRP user network mode, that will be connected to the host. We also forward the local port `10022` to the port `22` in the VM. So we can SSH into the node with: ```bash ssh root@localhost -p 10022 ``` # PXE Boot with QEMU We can configure `dnsmasq` also to serve the TFTP assets that are required for the [PXE Boot](../README.md#pxe-boot) image. For example, this can be used as a base for a local server: ```bash mkdir tftpboot sudo ./dnsmasq --no-daemon \ --interface=vmlan \ --except-interface=lo \ --except-interface=em1 \ --bind-interfaces \ --dhcp-range=10.0.3.100,10.0.3.200 \ --dhcp-option=option:router,10.0.3.101 \ --dhcp-host=00:00:00:11:11:11,10.0.3.101,worker \ --host-record=master,10.0.2.2 \ --enable-tftp \ --dhcp-boot=pxelinux.0,,10.0.3.1 \ --tftp-root=$(pwd)/tftpboot ``` Follow the documentation to create the different configuration files and copy the assets in the correct places. 07070100000006000041ED0000000000000000000000036130D1CF00000000000000000000000000000000000000000000003000000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples07070100000007000041ED0000000000000000000000046130D1CF00000000000000000000000000000000000000000000003600000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples/kubic07070100000008000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples/kubic/kubic07070100000009000081A40000000000000000000000016130D1CF000005B1000000000000000000000000000000000000004E00000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples/kubic/kubic/control_plane.sls{% import 'macros.yml' as macros %} {% set users = pillar['users'] %} {% set public_ip = grains['ip4_interfaces']['ens3'][0] %} {{ macros.log('module', 'install_kubic') }} install_kubic: module.run: - kubeadm.init: - apiserver_advertise_address: {{ public_ip }} - pod_network_cidr: '10.244.0.0/16' - creates: /etc/kubernetes/admin.conf {% for user in users %} {% set username = user.username %} {{ macros.log('file', 'create_kubic_directory_' ~ username) }} create_kubic_directory_{{ username }}: file.directory: - name: ~{{ username }}/.kube - user: {{ username }} - group: {{ username if username == 'root' else 'users' }} - mode: 700 {{ macros.log('file', 'copy_kubic_configuration_' ~ username) }} copy_kubic_configuration_{{ username }}: file.copy: - name: ~{{ username }}/.kube/config - source: /etc/kubernetes/admin.conf - user: {{ username }} - group: {{ username if username == 'root' else 'users' }} - mode: 700 {% endfor %} {{ macros.log('cmd', 'install_network') }} install_network: cmd.run: - name: kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml - unless: ip link | grep -q flannel {{ macros.log('loop', 'wait_interfaces_up') }} wait_interfaces_up: loop.until: - name: network.interfaces - condition: "'flannel.1' in m_ret" - period: 5 - timeout: 300 0707010000000A000081A40000000000000000000000016130D1CF000002CC000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples/kubic/kubic/join.sls{% import 'macros.yml' as macros %} {% set users = pillar['users'] %} {% set join_params = salt.mine.get(tgt='00:00:00:11:11:11', fun='join_params')['00:00:00:11:11:11'] %} {{ macros.log('module', 'join_control_plane') }} join_control_plane: module.run: - kubeadm.join: - api_server_endpoint: {{ join_params['api-server-endpoint'] }} - discovery_token_ca_cert_hash: {{ join_params['discovery-token-ca-cert-hash'] }} - token: {{ join_params['token'] }} - creates: /etc/kubernetes/kubelet.conf {{ macros.log('loop', 'wait_interfaces_up') }} wait_interfaces_up: loop.until: - name: network.interfaces - condition: "'flannel.1' in m_ret" - period: 5 - timeout: 300 0707010000000B000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples/kubic/orch0707010000000C000081A40000000000000000000000016130D1CF00000345000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/docs/examples/kubic/orch/kubic.slssynchronize_all: salt.function: - name: saltutil.sync_all - tgt: '*' install_microos: salt.state: - sls: - yomi - tgt: '*' wait_for_reboots: salt.wait_for_event: - name: salt/minion/*/start - id_list: - '00:00:00:11:11:11' - '00:00:00:22:22:22' - require: - salt: install_microos install_control_plane: salt.state: - tgt: '00:00:00:11:11:11' - sls: - kubic.control_plane send_mine: salt.function: - name: mine.send - tgt: '00:00:00:11:11:11' - arg: - join_params - kwarg: mine_function: kubeadm.join_params create_if_needed: yes join_worker: salt.state: - tgt: '00:00:00:22:22:22' - sls: - kubic.join delete_mine: salt.function: - name: mine.delete - tgt: '*' - arg: - join_params 0707010000000D000081A40000000000000000000000016130D1CF0000176E000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/docs/use-case-as-a-kubic-worker.md# Use Case: A Kubic worker provisioned with Yomi We can use [Yomi](https://github.com/openSUSE/yomi) to deploy worker nodes in an already deployed Kubic cluster. ## Overview and requirements In this section we are going to describe a way to deploy a two-node Kubic cluster, and use Yomi to provision a third node. For this example we can use `libvirt`, `virtualbox`, `vagrant` or `QEMU`. We will need to allocate three VMs with: * 50GB of hard disk * 2 CPU per node * 2048MB RAM per system We will need also connectivity bewteen the different VMs to form a local network, and also access to Internet for downloading packages. You can check [appendix-how-to-use-qemu.md](appendix-how-to-use-qemu.md) to learn about how to do this with QEMU and how to setup a DNS server with `dnsmasq` to create a network configuration that will meet the requirements. ## Installing MicroOS for Kubic Follow the documentation about how to install a two node Kubic cluster from the [Kubic documentation](https://en.opensuse.org/Kubic:kubeadm). In a nutshell the process is: * Spin two empty nodes with QEMU / libvirt * Boot both nodes using the [Kubic image](http://download.opensuse.org/tumbleweed/iso/openSUSE-Kubic-DVD-x86_64-Current.iso) * Deploy one node with the 'Kubic Admin Node' role, this will install CRI-O, `kubeadn` and `kubicctl`, together with `salt-master`. * Deploy the second node with the system role 'Kubic Worker Node'. We will use `kubicctl` to deploy Kubernetes in the control plane, and use this same tool to join the already installed worker. If the control plane node have more that one interface (for example, if we use QEMU as described in the appendix documentation this will be the case, but not if we use libvirt), we need to identify the one that is visible from the worker node. We will pass the IP of this interface via the `--adv-addr` parameter. ```bash kubicctl init --adv-addr 10.0.3.101 ``` If there are not multiple interfaces and we want to use `flannel` as a pod network, as simple `kubicctl init` will work on most of the cases. In the worker node we need to set up `salt-minion` so in can connect to the `salt-master` in the control plane node. We need to find the address or IP address that can be used to point to the master, configure the minion and restart the service. ```bash echo "master: <MASTER-IP>" > /etc/salt/minion.d/master.conf systemctl enable --now salt-minion.service ``` The minion now try to connect to the master, but before this can succeed we need to accept the key in the `master` node. ```bash salt-key -A ``` We can test from the master that the minion is answering properly: ```bash salt worker1 test.ping ``` Now we can join the node from the `master` one: ```bash kubicctl node add worker1 ``` Note that `worker1` is refers here to the minion ID that Salt recognize, not the host name of the worker node. If the command succeed, we inspect the cluster status: ```bash kubectl get nodes ``` It will show something like: ``` NAME STATUS ROLES AGE VERSION master Ready master 11m v1.15.0 worker1 Ready <none> 56s v1.15.0 ``` If `kubectl` fails, check that `/etc/kubernetes/admin.conf` is copied as `~/.kube/config` as documented in `kubeadm`. ## Provisioning a Kubic worker with Yomi The first worker was allocated via the Kubic DVD image. This is reasonable for small clusters, but we can simplify the work if we can install MicroOS on new nodes using SaltStack and later join the node to the cluster with `kubicctl`. ### Yomi image and Yomi package Yomi is a set of Salt states that will allows the provisioning of systems. We will need to boot the new node using a JeOS image that contains a `salt-minion` service, that later can be controlled from the `master` node, that is where `salt-master` is installed. You can find mode information about this Yomi image in the [Booting a new machine](../README.md#booting-a-new-machine) section if the main documentation. Download the ISO image or the PXE Boot one (check the previous link the learn how to configure the PXE Boot one). Optionally configure the `salt-master` to enable the auto-sign feature via UUID, as described in the [Enabling auto-sign](../README.md#enabling-auto-sign) section. In the `master` node we will need to install the `yomi-formula` package from Factory. ```bash transactional-update pkg install yomi-formula reboot ``` We can now boot a new node in the same network that the Kubic cluster, using the Yomi image. Be sure (via boot parameter or later configuration) that the `salt-minion` can find the already present master, and if needed accept the key. ### Adding the new worker We need to set the pillar data that Yomi will use to make a new installation. This data will describe installation details like how will be the hard disk partition layout, the different packages that will be installed or the services that will be enabled before booting. The packages `yomi-formula` already provides and example for a MicroOS installation, so we can use it as a template. Read the section [Configuring the pillar](../README.md#configuring-the-pillar) to learn more about the pillar examples provided by the package, and how to copy them in a place that we can edit them. The `yomi-formula` package do not include an example `top.sls`, but we can create one easily for this example. ```bash cat <<EOF > /srv/salt/top.sls base: '*': - yomi EOF ``` Check also that for the pillar we also have a `top.sls`. The one that we have in the package as an example is: ```yaml base: '*': - installer ``` Now we can [get information about the hardware](../README.md#getting-hardware-information) available in the new worker node, and adjust the pillar accordingly. Optionally we can [wipe the disks](../README.md#cleaning-the-disks), and then apply the `yomi` state. ```bash salt worker2 state.apply ``` Once the node is back, we can proceed as usual: ```bash kubicctl node add worker2 ``` 0707010000000E000081A40000000000000000000000016130D1CF0000132C000000000000000000000000000000000000005000000000yomi-0.0.1+git.1630589391.4557cfd/docs/use-case-deploying-kubic-from-scratch.md# Use Case: Deployment of Kubic from scratch We can use [Yomi](https://github.com/openSUSE/yomi) to deploy the control plane and the workers of a new Kubic cluster using SaltStack to orchestrate the installation. ## Deploying a Kubic control plane node with Yomi In this section we are going to describe a way to deploy a two-node Kubic cluster from scratch. One node will be the controller or the Kubic cluster, and the second node will be the worker. For this example we can use `libvirt`, `virtualbox`, `vagrant` or `QEMU`. We will need to allocate two VMs with: * 50GB of hard disk * 2 CPU per node * 2048MB RAM per system We will need also connectivity bewteen the different VMs to form a local network, and also access to Internet for downloading packages. You can check [appendix-how-to-use-qemu.md](appendix-how-to-use-qemu.md) to learn about how to do this with QEMU and how to setup a DNS server with `dnsmasq` to create a network configuration that will meet the requirements. The general process will be to install a local `salt-master`, that will be used to first install MicroOS in the two VMs. Later we will use a [Salt orchestrator](https://docs.saltstack.com/en/latest/topics/orchestrate/orchestrate_runner.html) to provision the operating system and install the different Kubic components via `kubeadm`. One node of the cluster will be for the control plane, and the second one will be a worker. ## Installing salt-master and yomi-formula We need to install locally the `salt-master` and the `yomi-formula` packages, as we will control the installation from out laptop or desktop machine. ```bash sudo zypper in salt-master salt-standalone-formulas-configuration sudo zypper in yomi-formula ``` ## Configuring salt-master We are going to use the states from Yomi that are living in `/usr/share/salt-formulas/yomi`, and some other states are are in `/usr/share/yomi/kubic`. In order to make both location reachable, we need to configure `salt-master`. ```bash sudo cp -a /usr/share/yomi/kubic-file.conf /etc/salt/master.d/ sudo cp -a /usr/share/yomi/pillar.conf /etc/salt/master.d/ ``` Optionally, we will configure autosign via UUID, so we can avoid accept the new `salt-minion` keys during the exercise. ```bash sudo cp /usr/share/yomi/autosign.conf /etc/salt/master.d/ ``` We can now restart the service: ```bash systemctl restart salt-master.service ``` For a more detailed description read the sections [Looking for the pillar](../README.md#looking-for-the-pillar) and [Enabling auto-sign](../README.md#enabling-auto-sign) in the documentation. ## Orchestrating the Kubic installation Now we can launch two nodes via `libvirt` or `QEMU`. For this last option read the document [How to use QEMU](appendix-how-to-use-qemu.md) to take some ideas and make the proper adjustments on `dnsmasq` to assign correct names for the different nodes. You need to boot both nodes with the ISO image or the PXE Boot one, and check that you can see them locally: ```bash salt '*' test.ping ``` If something goes wrong check this in order: 1. `master` can be resolved from the nodes 2. `salt-minion` service is running correctly 3. There is no old key in the master (`salt-key '*' -D`) Adjust the `kubic.sls` from the states to reference properly the nodes. The provided example is using the MAC address to reference the nodes: * `00:00:00:11:11:11`: Control plane node * `00:00:00:22:22:22`: Worker node Now we can orchestrate the Kubic installation. So from your host machine where `salt-master` is running we can fire the orchestrator. ```bash salt-run state.orchestrate orch.kubic ``` This will execute commands in the `salt-master`, that will: 1. Synchronize all the execution modules, pillars and grains 2. Install MicroOS in both nodes 3. Wait for the reboot of both nodes 4. Install the control plane in `00:00:00:11:11:11` 5. Send a mine to the control plane node, that will collect the connection secrets 6. Join the worker (`00:00:00:22:22:22`) using `kubeadm` and those secrets 7. Remove the mine This orchestrator is only an example, and there are elements that can be improved. The main one is that inside the YAML file there are references to the minion ID of the control plane and the worker, something that is better to put in the pillar. Another problem is that in the current version of Salt, we cannot send asynchronous commands to the orchestrator. This imply that there is a race condition in the section that wait for the node reboot. If one node reboot before than the other, there is a chance that the reboot event will be lost before the `salt.wait_for_event` is reached. The next version of Salt, Neon, will add this capability, and the example will be updated accordingly. If this race condition happens, you can wait manually to the reboot, comment the `salt.wait_for_event` entry in `kubic.sls`, and relaunch the `salt-run` command. 0707010000000F000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000002B00000000yomi-0.0.1+git.1630589391.4557cfd/metadata07070100000010000081A40000000000000000000000016130D1CF00006D28000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/metadata/01-form-yomi.yml# Uyuni Form for the Yomi pillar data - Main section # # Find mode pillar examples in /usr/share/yomi/pillar config: $name: General Configuration $type: group $help: General Configuration Section for the Yomi Formula events: $type: boolean # Change default once Uyuni can track the events $default: no $help: If set, the installation can be monitored via Salt events reboot: $type: select $values: - "yes" - "no" - kexec - halt - shutdown $default: "yes" $help: Kind of reboot at the end of the installation snapper: $type: boolean $default: no $help: For now it only can be used in Btrfs filesystems # TODO: How to export the values from the locale formula? locale: $type: select # Output from 'localectl list-locales' $values: - C.utf8 - aa_DJ - aa_DJ.utf8 - aa_ER - aa_ER@saaho - aa_ET - af_ZA - af_ZA.utf8 - agr_PE - ak_GH - am_ET - an_ES - an_ES.utf8 - anp_IN - ar_AE - ar_AE.utf8 - ar_BH - ar_BH.utf8 - ar_DZ - ar_DZ.utf8 - ar_EG - ar_EG.utf8 - ar_IN - ar_IQ - ar_IQ.utf8 - ar_JO - ar_JO.utf8 - ar_KW - ar_KW.utf8 - ar_LB - ar_LB.utf8 - ar_LY - ar_LY.utf8 - ar_MA - ar_MA.utf8 - ar_OM - ar_OM.utf8 - ar_QA - ar_QA.utf8 - ar_SA - ar_SA.utf8 - ar_SD - ar_SD.utf8 - ar_SS - ar_SY - ar_SY.utf8 - ar_TN - ar_TN.utf8 - ar_YE - ar_YE.utf8 - as_IN - ast_ES - ast_ES.utf8 - ayc_PE - az_AZ - az_IR - be_BY - be_BY.utf8 - be_BY@latin - bem_ZM - ber_DZ - ber_MA - bg_BG - bg_BG.utf8 - bhb_IN.utf8 - bho_IN - bi_VU - bn_BD - bn_IN - bo_CN - bo_IN - br_FR - br_FR.utf8 - br_FR@euro - brx_IN - bs_BA - bs_BA.utf8 - byn_ER - ca_AD - ca_AD.utf8 - ca_ES - ca_ES.utf8 - ca_ES@euro - ca_FR - ca_FR.utf8 - ca_IT - ca_IT.utf8 - ce_RU - chr_US - cmn_TW - crh_UA - cs_CZ - cs_CZ.utf8 - csb_PL - cv_RU - cy_GB - cy_GB.utf8 - da_DK - da_DK.utf8 - de_AT - de_AT.utf8 - de_AT@euro - de_BE - de_BE.utf8 - de_BE@euro - de_CH - de_CH.utf8 - de_DE - de_DE.utf8 - de_DE@euro - de_IT - de_IT.utf8 - de_LI.utf8 - de_LU - de_LU.utf8 - de_LU@euro - doi_IN - dv_MV - dz_BT - el_CY - el_CY.utf8 - el_GR - el_GR.utf8 - el_GR@euro - en_AG - en_AU - en_AU.utf8 - en_BW - en_BW.utf8 - en_CA - en_CA.utf8 - en_DK - en_DK.utf8 - en_GB - en_GB.iso885915 - en_GB.utf8 - en_HK - en_HK.utf8 - en_IE - en_IE.utf8 - en_IE@euro - en_IL - en_IN - en_NG - en_NZ - en_NZ.utf8 - en_PH - en_PH.utf8 - en_SG - en_SG.utf8 - en_US - en_US.iso885915 - en_US.utf8 - en_ZA - en_ZA.utf8 - en_ZM - en_ZW - en_ZW.utf8 - eo - es_AR - es_AR.utf8 - es_BO - es_BO.utf8 - es_CL - es_CL.utf8 - es_CO - es_CO.utf8 - es_CR - es_CR.utf8 - es_CU - es_DO - es_DO.utf8 - es_EC - es_EC.utf8 - es_ES - es_ES.utf8 - es_ES@euro - es_GT - es_GT.utf8 - es_HN - es_HN.utf8 - es_MX - es_MX.utf8 - es_NI - es_NI.utf8 - es_PA - es_PA.utf8 - es_PE - es_PE.utf8 - es_PR - es_PR.utf8 - es_PY - es_PY.utf8 - es_SV - es_SV.utf8 - es_US - es_US.utf8 - es_UY - es_UY.utf8 - es_VE - es_VE.utf8 - et_EE - et_EE.iso885915 - et_EE.utf8 - eu_ES - eu_ES.utf8 - eu_ES@euro - fa_IR - ff_SN - fi_FI - fi_FI.utf8 - fi_FI@euro - fil_PH - fo_FO - fo_FO.utf8 - fr_BE - fr_BE.utf8 - fr_BE@euro - fr_CA - fr_CA.utf8 - fr_CH - fr_CH.utf8 - fr_FR - fr_FR.utf8 - fr_FR@euro - fr_LU - fr_LU.utf8 - fr_LU@euro - fur_IT - fy_DE - fy_NL - ga_IE - ga_IE.utf8 - ga_IE@euro - gd_GB - gd_GB.utf8 - gez_ER - gez_ER@abegede - gez_ET - gez_ET@abegede - gl_ES - gl_ES.utf8 - gl_ES@euro - gu_IN - gv_GB - gv_GB.utf8 - ha_NG - hak_TW - he_IL - he_IL.utf8 - hi_IN - hif_FJ - hne_IN - hr_HR - hr_HR.utf8 - hsb_DE - hsb_DE.utf8 - ht_HT - hu_HU - hu_HU.utf8 - hy_AM - hy_AM.armscii8 - ia_FR - id_ID - id_ID.utf8 - ig_NG - ik_CA - is_IS - is_IS.utf8 - it_CH - it_CH.utf8 - it_IT - it_IT.utf8 - it_IT@euro - iu_CA - ja_JP.eucjp - ja_JP.shiftjisx0213 - ja_JP.sjis - ja_JP.utf8 - ka_GE - ka_GE.utf8 - kk_KZ - kk_KZ.utf8 - kl_GL - kl_GL.utf8 - km_KH - kn_IN - ko_KR.euckr - ko_KR.utf8 - kok_IN - ks_IN - ks_IN@devanagari - ku_TR - ku_TR.utf8 - kw_GB - kw_GB.utf8 - ky_KG - lb_LU - lg_UG - lg_UG.utf8 - li_BE - li_NL - lij_IT - ln_CD - lo_LA - lt_LT - lt_LT.utf8 - lv_LV - lv_LV.utf8 - lzh_TW - mag_IN - mai_IN - mai_NP - mg_MG - mg_MG.utf8 - mhr_RU - mi_NZ - mi_NZ.utf8 - mk_MK - mk_MK.utf8 - ml_IN - mn_MN - mni_IN - mr_IN - ms_MY - ms_MY.utf8 - mt_MT - mt_MT.utf8 - my_MM - nan_TW - nan_TW@latin - nb_NO - nb_NO.utf8 - nds_DE - nds_NL - ne_NP - nhn_MX - niu_NU - niu_NZ - nl_AW - nl_BE - nl_BE.utf8 - nl_BE@euro - nl_NL - nl_NL.utf8 - nl_NL@euro - nn_NO - nn_NO.utf8 - no_NO - no_NO.utf8 - nr_ZA - nso_ZA - oc_FR - oc_FR.utf8 - om_ET - om_KE - om_KE.utf8 - or_IN - os_RU - pa_IN - pa_PK - pap_AW - pap_CW - pl_PL - pl_PL.utf8 - ps_AF - pt_BR - pt_BR.utf8 - pt_PT - pt_PT.utf8 - pt_PT@euro - quz_PE - raj_IN - ro_RO - ro_RO.utf8 - ru_RU - ru_RU.koi8r - ru_RU.utf8 - ru_UA - ru_UA.utf8 - rw_RW - sa_IN - sat_IN - sc_IT - sd_IN - sd_IN@devanagari - se_NO - sgs_LT - shs_CA - si_LK - sid_ET - sk_SK - sk_SK.utf8 - sl_SI - sl_SI.utf8 - sm_WS - so_DJ - so_DJ.utf8 - so_ET - so_KE - so_KE.utf8 - so_SO - so_SO.utf8 - sq_AL - sq_AL.utf8 - sq_MK - sr_ME - sr_RS - sr_RS@latin - ss_ZA - st_ZA - st_ZA.utf8 - sv_FI - sv_FI.utf8 - sv_FI@euro - sv_SE - sv_SE.utf8 - sw_KE - sw_TZ - szl_PL - ta_IN - ta_LK - tcy_IN.utf8 - te_IN - tg_TJ - tg_TJ.utf8 - th_TH - th_TH.utf8 - the_NP - ti_ER - ti_ET - tig_ER - tk_TM - tl_PH - tl_PH.utf8 - tn_ZA - to_TO - tpi_PG - tr_CY - tr_CY.utf8 - tr_TR - tr_TR.utf8 - ts_ZA - tt_RU - tt_RU@iqtelif - ug_CN - uk_UA - uk_UA.utf8 - unm_US - ur_IN - ur_PK - uz_UZ - uz_UZ.utf8 - uz_UZ@cyrillic - ve_ZA - vi_VN - wa_BE - wa_BE.utf8 - wa_BE@euro - wae_CH - wal_ET - wo_SN - xh_ZA - xh_ZA.utf8 - yi_US - yi_US.utf8 - yo_NG - yue_HK - zh_CN - zh_CN.gb18030 - zh_CN.gbk - zh_CN.utf8 - zh_HK - zh_HK.utf8 - zh_SG - zh_SG.gbk - zh_SG.utf8 - zh_TW - zh_TW.euctw - zh_TW.utf8 - zu_ZA - zu_ZA.utf8 $default: en_US.utf8 $help: System locale configuration for systemd keymap: $type: select # Output from 'localectl list-keymaps' $values: - ANSI-dvorak - Pl02 - al - al-plisi - amiga-de - amiga-us - applkey - at - at-mac - at-nodeadkeys - at-sundeadkeys - atari-de - atari-se - atari-uk-falcon - atari-us - az - azerty - ba - ba-alternatequotes - ba-unicode - ba-unicodeus - ba-us - backspace - bashkir - be - be-iso-alternate - be-latin1 - be-nodeadkeys - be-oss - be-oss_latin9 - be-oss_sundeadkeys - be-sundeadkeys - be-wang - bg-cp1251 - bg-cp855 - bg_bds-cp1251 - bg_bds-utf8 - bg_pho-cp1251 - bg_pho-utf8 - br - br-abnt - br-abnt-alt - br-abnt2 - br-abnt2-old - br-dvorak - br-latin1-abnt2 - br-latin1-us - br-nativo - br-nativo-epo - br-nativo-us - br-nodeadkeys - br-thinkpad - by - by-cp1251 - by-latin - bywin-cp1251 - ca - ca-eng - ca-fr-dvorak - ca-fr-legacy - ca-multi - ca-multix - carpalx - carpalx-full - cf - ch - ch-de_mac - ch-de_nodeadkeys - ch-de_sundeadkeys - ch-fr - ch-fr_mac - ch-fr_nodeadkeys - ch-fr_sundeadkeys - ch-legacy - cm - cm-azerty - cm-dvorak - cm-french - cm-mmuock - cm-qwerty - cn - cn-latin1 - croat - ctrl - cz - cz-bksl - cz-cp1250 - cz-dvorak-ucw - cz-lat2 - cz-lat2-prog - cz-lat2-us - cz-qwerty - cz-qwerty_bksl - cz-rus - cz-us-qwertz - de - de-T3 - de-deadacute - de-deadgraveacute - de-deadtilde - de-dsb - de-dsb_qwertz - de-dvorak - de-latin1 - de-latin1-nodeadkeys - de-mac - de-mac_nodeadkeys - de-mobii - de-neo - de-nodeadkeys - de-qwerty - de-ro - de-ro_nodeadkeys - de-sundeadkeys - de-tr - de_CH-latin1 - de_alt_UTF-8 - defkeymap - defkeymap_V1.0 - dk - dk-dvorak - dk-latin1 - dk-mac - dk-mac_nodeadkeys - dk-nodeadkeys - dk-winkeys - dvorak - dvorak-ca-fr - dvorak-es - dvorak-fr - dvorak-l - dvorak-la - dvorak-programmer - dvorak-r - dvorak-ru - dvorak-sv-a1 - dvorak-sv-a5 - dvorak-uk - dz - ee - ee-dvorak - ee-nodeadkeys - ee-us - emacs - emacs2 - en-latin9 - epo - epo-legacy - es - es-ast - es-cat - es-cp850 - es-deadtilde - es-dvorak - es-mac - es-nodeadkeys - es-olpc - es-sundeadkeys - es-winkeys - et - et-nodeadkeys - euro - euro1 - euro2 - fi - fi-classic - fi-kotoistus - fi-mac - fi-nodeadkeys - fi-smi - fi-winkeys - fo - fo-nodeadkeys - fr - fr-azerty - fr-bepo - fr-bepo-latin9 - fr-bepo_latin9 - fr-bre - fr-dvorak - fr-latin1 - fr-latin9 - fr-latin9_nodeadkeys - fr-latin9_sundeadkeys - fr-mac - fr-nodeadkeys - fr-oci - fr-oss - fr-oss_latin9 - fr-oss_nodeadkeys - fr-oss_sundeadkeys - fr-pc - fr-sundeadkeys - fr_CH - fr_CH-latin1 - gb - gb-colemak - gb-dvorak - gb-dvorakukp - gb-extd - gb-intl - gb-mac - gb-mac_intl - ge - ge-ergonomic - ge-mess - ge-ru - gh - gh-akan - gh-avn - gh-ewe - gh-fula - gh-ga - gh-generic - gh-gillbt - gh-hausa - gr - gr-pc - hr - hr-alternatequotes - hr-unicode - hr-unicodeus - hr-us - hu - hu-101_qwerty_comma_dead - hu-101_qwerty_comma_nodead - hu-101_qwerty_dot_dead - hu-101_qwerty_dot_nodead - hu-101_qwertz_comma_dead - hu-101_qwertz_comma_nodead - hu-101_qwertz_dot_dead - hu-101_qwertz_dot_nodead - hu-102_qwerty_comma_dead - hu-102_qwerty_comma_nodead - hu-102_qwerty_dot_dead - hu-102_qwerty_dot_nodead - hu-102_qwertz_comma_dead - hu-102_qwertz_comma_nodead - hu-102_qwertz_dot_dead - hu-102_qwertz_dot_nodead - hu-nodeadkeys - hu-qwerty - hu-standard - hu101 - ie - ie-CloGaelach - ie-UnicodeExpert - ie-ogam_is434 - il - il-heb - il-phonetic - in-eng - iq-ku - iq-ku_alt - iq-ku_ara - iq-ku_f - ir-ku - ir-ku_alt - ir-ku_ara - ir-ku_f - is - is-Sundeadkeys - is-dvorak - is-latin1 - is-latin1-us - is-mac - is-mac_legacy - is-nodeadkeys - it - it-geo - it-ibm - it-intl - it-mac - it-nodeadkeys - it-scn - it-us - it-winkeys - it2 - jp - jp-OADG109A - jp-dvorak - jp-kana86 - jp106 - kazakh - ke - ke-kik - keypad - kr - kr-kr104 - ky_alt_sh-UTF-8 - kyrgyz - la-latin1 - latam - latam-deadtilde - latam-dvorak - latam-nodeadkeys - latam-sundeadkeys - lk-us - lt - lt-ibm - lt-lekp - lt-lekpa - lt-std - lt-us - lt.baltic - lt.l4 - lt.std - lv - lv-adapted - lv-apostrophe - lv-ergonomic - lv-fkey - lv-modern - lv-tilde - ma-french - mac-be - mac-de-latin1 - mac-de-latin1-nodeadkeys - mac-de_CH - mac-dk-latin1 - mac-dvorak - mac-es - mac-euro - mac-euro2 - mac-fi-latin1 - mac-fr - mac-fr_CH-latin1 - mac-it - mac-pl - mac-pt-latin1 - mac-se - mac-template - mac-uk - mac-us - md - md-gag - me - me-latinalternatequotes - me-latinunicode - me-latinunicodeyz - me-latinyz - mk - mk-cp1251 - mk-utf - mk0 - ml - ml-fr-oss - ml-us-intl - ml-us-mac - mt - mt-us - ng - ng-hausa - ng-igbo - ng-yoruba - nl - nl-mac - nl-std - nl-sundeadkeys - nl2 - "no" - no-colemak - no-dvorak - no-latin1 - no-mac - no-mac_nodeadkeys - no-nodeadkeys - no-smi - no-smi_nodeadkeys - no-winkeys - pc110 - ph - ph-capewell-dvorak - ph-capewell-qwerf2k6 - ph-colemak - ph-dvorak - pl - pl-csb - pl-dvorak - pl-dvorak_altquotes - pl-dvorak_quotes - pl-dvp - pl-legacy - pl-qwertz - pl-szl - pl1 - pl2 - pl3 - pl4 - pt - pt-latin1 - pt-latin9 - pt-mac - pt-mac_nodeadkeys - pt-mac_sundeadkeys - pt-nativo - pt-nativo-epo - pt-nativo-us - pt-nodeadkeys - pt-sundeadkeys - ro - ro-cedilla - ro-latin2 - ro-std - ro-std_cedilla - ro-winkeys - ro_std - ro_win - rs-latin - rs-latinalternatequotes - rs-latinunicode - rs-latinunicodeyz - rs-latinyz - ru - ru-cp1251 - ru-cv_latin - ru-ms - ru-yawerty - ru1 - ru1_win-utf - ru2 - ru3 - ru4 - ru_win - ruwin_alt-CP1251 - ruwin_alt-KOI8-R - ruwin_alt-UTF-8 - ruwin_alt_sh-UTF-8 - ruwin_cplk-CP1251 - ruwin_cplk-KOI8-R - ruwin_cplk-UTF-8 - ruwin_ct_sh-CP1251 - ruwin_ct_sh-KOI8-R - ruwin_ct_sh-UTF-8 - ruwin_ctrl-CP1251 - ruwin_ctrl-KOI8-R - ruwin_ctrl-UTF-8 - se - se-dvorak - se-fi-ir209 - se-fi-lat6 - se-ir209 - se-lat6 - se-latin1 - se-mac - se-nodeadkeys - se-smi - se-svdvorak - se-us_dvorak - sg - sg-latin1 - sg-latin1-lk450 - si - si-alternatequotes - si-us - sk - sk-bksl - sk-prog-qwerty - sk-prog-qwertz - sk-qwerty - sk-qwerty_bksl - sk-qwertz - slovene - sr-cy - sun-pl - sun-pl-altgraph - sundvorak - sunkeymap - sunt4-es - sunt4-fi-latin1 - sunt4-no-latin1 - sunt5-cz-us - sunt5-de-latin1 - sunt5-es - sunt5-fi-latin1 - sunt5-fr-latin1 - sunt5-ru - sunt5-uk - sunt5-us-cz - sunt6-uk - sv-latin1 - sy-ku - sy-ku_alt - sy-ku_f - tj_alt-UTF8 - tm - tm-alt - tr - tr-alt - tr-crh - tr-crh_alt - tr-crh_f - tr-f - tr-intl - tr-ku - tr-ku_alt - tr-ku_f - tr-sundeadkeys - tr_f-latin5 - tr_q-latin5 - tralt - trf - trq - ttwin_alt-UTF-8 - ttwin_cplk-UTF-8 - ttwin_ct_sh-UTF-8 - ttwin_ctrl-UTF-8 - tw - tw-indigenous - tw-saisiyat - ua - ua-cp1251 - ua-utf - ua-utf-ws - ua-ws - uk - unicode - us - us-acentos - us-acentos-old - us-alt-intl - us-altgr-intl - us-colemak - us-dvorak - us-dvorak-alt-intl - us-dvorak-classic - us-dvorak-intl - us-dvorak-l - us-dvorak-r - us-dvp - us-euro - us-hbs - us-intl - us-mac - us-olpc2 - us-workman - us-workman-intl - uz-latin - wangbe - wangbe2 - windowkeys $default: us $help: System keyboard configuration for systemd timezone: $type: select # Output from 'timedatectl list-timezones' $values: - Africa/Abidjan - Africa/Accra - Africa/Algiers - Africa/Bissau - Africa/Cairo - Africa/Casablanca - Africa/Ceuta - Africa/El_Aaiun - Africa/Johannesburg - Africa/Juba - Africa/Khartoum - Africa/Lagos - Africa/Maputo - Africa/Monrovia - Africa/Nairobi - Africa/Ndjamena - Africa/Sao_Tome - Africa/Tripoli - Africa/Tunis - Africa/Windhoek - America/Adak - America/Anchorage - America/Araguaina - America/Argentina/Buenos_Aires - America/Argentina/Catamarca - America/Argentina/Cordoba - America/Argentina/Jujuy - America/Argentina/La_Rioja - America/Argentina/Mendoza - America/Argentina/Rio_Gallegos - America/Argentina/Salta - America/Argentina/San_Juan - America/Argentina/San_Luis - America/Argentina/Tucuman - America/Argentina/Ushuaia - America/Asuncion - America/Atikokan - America/Bahia - America/Bahia_Banderas - America/Barbados - America/Belem - America/Belize - America/Blanc-Sablon - America/Boa_Vista - America/Bogota - America/Boise - America/Cambridge_Bay - America/Campo_Grande - America/Cancun - America/Caracas - America/Cayenne - America/Chicago - America/Chihuahua - America/Costa_Rica - America/Creston - America/Cuiaba - America/Curacao - America/Danmarkshavn - America/Dawson - America/Dawson_Creek - America/Denver - America/Detroit - America/Edmonton - America/Eirunepe - America/El_Salvador - America/Fort_Nelson - America/Fortaleza - America/Glace_Bay - America/Godthab - America/Goose_Bay - America/Grand_Turk - America/Guatemala - America/Guayaquil - America/Guyana - America/Halifax - America/Havana - America/Hermosillo - America/Indiana/Indianapolis - America/Indiana/Knox - America/Indiana/Marengo - America/Indiana/Petersburg - America/Indiana/Tell_City - America/Indiana/Vevay - America/Indiana/Vincennes - America/Indiana/Winamac - America/Inuvik - America/Iqaluit - America/Jamaica - America/Juneau - America/Kentucky/Louisville - America/Kentucky/Monticello - America/La_Paz - America/Lima - America/Los_Angeles - America/Maceio - America/Managua - America/Manaus - America/Martinique - America/Matamoros - America/Mazatlan - America/Menominee - America/Merida - America/Metlakatla - America/Mexico_City - America/Miquelon - America/Moncton - America/Monterrey - America/Montevideo - America/Nassau - America/New_York - America/Nipigon - America/Nome - America/Noronha - America/North_Dakota/Beulah - America/North_Dakota/Center - America/North_Dakota/New_Salem - America/Ojinaga - America/Panama - America/Pangnirtung - America/Paramaribo - America/Phoenix - America/Port-au-Prince - America/Port_of_Spain - America/Porto_Velho - America/Puerto_Rico - America/Punta_Arenas - America/Rainy_River - America/Rankin_Inlet - America/Recife - America/Regina - America/Resolute - America/Rio_Branco - America/Santarem - America/Santiago - America/Santo_Domingo - America/Sao_Paulo - America/Scoresbysund - America/Sitka - America/St_Johns - America/Swift_Current - America/Tegucigalpa - America/Thule - America/Thunder_Bay - America/Tijuana - America/Toronto - America/Vancouver - America/Whitehorse - America/Winnipeg - America/Yakutat - America/Yellowknife - Antarctica/Casey - Antarctica/Davis - Antarctica/DumontDUrville - Antarctica/Macquarie - Antarctica/Mawson - Antarctica/Palmer - Antarctica/Rothera - Antarctica/Syowa - Antarctica/Troll - Antarctica/Vostok - Asia/Almaty - Asia/Amman - Asia/Anadyr - Asia/Aqtau - Asia/Aqtobe - Asia/Ashgabat - Asia/Atyrau - Asia/Baghdad - Asia/Baku - Asia/Bangkok - Asia/Barnaul - Asia/Beirut - Asia/Bishkek - Asia/Brunei - Asia/Chita - Asia/Choibalsan - Asia/Colombo - Asia/Damascus - Asia/Dhaka - Asia/Dili - Asia/Dubai - Asia/Dushanbe - Asia/Famagusta - Asia/Gaza - Asia/Hebron - Asia/Ho_Chi_Minh - Asia/Hong_Kong - Asia/Hovd - Asia/Irkutsk - Asia/Jakarta - Asia/Jayapura - Asia/Jerusalem - Asia/Kabul - Asia/Kamchatka - Asia/Karachi - Asia/Kathmandu - Asia/Khandyga - Asia/Kolkata - Asia/Krasnoyarsk - Asia/Kuala_Lumpur - Asia/Kuching - Asia/Macau - Asia/Magadan - Asia/Makassar - Asia/Manila - Asia/Nicosia - Asia/Novokuznetsk - Asia/Novosibirsk - Asia/Omsk - Asia/Oral - Asia/Pontianak - Asia/Pyongyang - Asia/Qatar - Asia/Qostanay - Asia/Qyzylorda - Asia/Riyadh - Asia/Sakhalin - Asia/Samarkand - Asia/Seoul - Asia/Shanghai - Asia/Singapore - Asia/Srednekolymsk - Asia/Taipei - Asia/Tashkent - Asia/Tbilisi - Asia/Tehran - Asia/Thimphu - Asia/Tokyo - Asia/Tomsk - Asia/Ulaanbaatar - Asia/Urumqi - Asia/Ust-Nera - Asia/Vladivostok - Asia/Yakutsk - Asia/Yangon - Asia/Yekaterinburg - Asia/Yerevan - Atlantic/Azores - Atlantic/Bermuda - Atlantic/Canary - Atlantic/Cape_Verde - Atlantic/Faroe - Atlantic/Madeira - Atlantic/Reykjavik - Atlantic/South_Georgia - Atlantic/Stanley - Australia/Adelaide - Australia/Brisbane - Australia/Broken_Hill - Australia/Currie - Australia/Darwin - Australia/Eucla - Australia/Hobart - Australia/Lindeman - Australia/Lord_Howe - Australia/Melbourne - Australia/Perth - Australia/Sydney - Europe/Amsterdam - Europe/Andorra - Europe/Astrakhan - Europe/Athens - Europe/Belgrade - Europe/Berlin - Europe/Brussels - Europe/Bucharest - Europe/Budapest - Europe/Chisinau - Europe/Copenhagen - Europe/Dublin - Europe/Gibraltar - Europe/Helsinki - Europe/Istanbul - Europe/Kaliningrad - Europe/Kiev - Europe/Kirov - Europe/Lisbon - Europe/London - Europe/Luxembourg - Europe/Madrid - Europe/Malta - Europe/Minsk - Europe/Monaco - Europe/Moscow - Europe/Oslo - Europe/Paris - Europe/Prague - Europe/Riga - Europe/Rome - Europe/Samara - Europe/Saratov - Europe/Simferopol - Europe/Sofia - Europe/Stockholm - Europe/Tallinn - Europe/Tirane - Europe/Ulyanovsk - Europe/Uzhgorod - Europe/Vienna - Europe/Vilnius - Europe/Volgograd - Europe/Warsaw - Europe/Zaporozhye - Europe/Zurich - Indian/Chagos - Indian/Christmas - Indian/Cocos - Indian/Kerguelen - Indian/Mahe - Indian/Maldives - Indian/Mauritius - Indian/Reunion - Pacific/Apia - Pacific/Auckland - Pacific/Bougainville - Pacific/Chatham - Pacific/Chuuk - Pacific/Easter - Pacific/Efate - Pacific/Enderbury - Pacific/Fakaofo - Pacific/Fiji - Pacific/Funafuti - Pacific/Galapagos - Pacific/Gambier - Pacific/Guadalcanal - Pacific/Guam - Pacific/Honolulu - Pacific/Kiritimati - Pacific/Kosrae - Pacific/Kwajalein - Pacific/Majuro - Pacific/Marquesas - Pacific/Nauru - Pacific/Niue - Pacific/Norfolk - Pacific/Noumea - Pacific/Pago_Pago - Pacific/Palau - Pacific/Pitcairn - Pacific/Pohnpei - Pacific/Port_Moresby - Pacific/Rarotonga - Pacific/Tahiti - Pacific/Tarawa - Pacific/Tongatapu - Pacific/Wake - Pacific/Wallis - UTC $default: UTC $help: System timezone configuration for systemd hostname: $type: text $optional: yes $help: Leave it empty when DHCP provides a hostname machine_id: $type: text $optional: yes $help: If empty, systemd will generate one target: $type: text $optional: yes $default: multi-user.target $ifEmpty: multi-user.target $help: Valid systemd target unit 07070100000011000081A40000000000000000000000016130D1CF000020CC000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/metadata/02-form-yomi-storage.yml# Uyuni Form for the Yomi pillar data - Storage and filesystem # # Find mode pillar examples in /usr/share/yomi/pillar partitions: $type: group $help: Partiton (Storage) Subsection for the Yomi Formula config: $type: group $help: Configuration options for the partitioner label: $type: select $values: # - aix # - amiga # - bsd # - dvh - gpt # - mac - msdos # - pc98 # - sun # - loop $default: gpt $help: Default type of partition table for the device initial_gap: $type: text $optional: yes $default: 0 $help: Initial gap (empty space) leaved before the first partition. Valid units are s, B, kB, MB, GB, TB, compact, cyl, chs, %, kiB, MiB, GiB, TiB devices: $type: edit-group $minItems: 1 $itemName: Device ${i} $help: List of (physical or logical) devices $prototype: $type: group $key: $name: Device $type: text $placeholder: /dev/sda $help: Device name. Names like /dev/disk/by-id/... or /dev/disk/by-label/... can be used label: $type: select $values: # - aix # - amiga # - bsd # - dvh - gpt # - mac - msdos # - pc98 # - sun # - loop $default: gpt $help: Type of partition table for the device initial_gap: $type: text $optional: yes $default: 1MB $help: Initial gap (empty space) leaved before the first partition. Valid units are s, B, kB, MB, GB, TB, compact, cyl, chs, %, kiB, MiB, GiB, TiB partitions: $type: edit-group $minItems: 0 $itemName: Partition ${i} $help: List of partitions for the device $prototype: number: $name: Partition Number $type: number $optional: yes # $default: ${i} $help: Will be appended to the device name (E.g. /dev/sda1 for devide /dev/sda and partition number 1) id: $name: Partition Name $type: text $optional: yes $placeholder: /dev/sda1 $help: "Full name of the partition. For example, valid ids can be /dev/sda1, /dev/md0p1, etc. Is optional, as the name can be deduced from 'Partition Number'" size: $name: Partition Size $type: text $placeholder: "Parted units or 'rest': 500MB" $help: "Valid units are s, B, kB, MB, GB, TB, compact, cyl, chs, %, kiB, MiB, GiB, TiB. Use 'rest' to indicate the rest of the free space" type: $name: Partition Type $type: select $values: - swap - linux - boot - efi - lvm - raid $default: linux $help: Indicate the expected use of the partition lvm: $type: edit-group $minItems: 0 $itemName: Volume Group ${i} $help: LVM (Storage) Subsection for the Yomi Formula $prototype: $type: group $key: $name: Volume Group Name $type: text $help: Name of the logical volume devices: $type: edit-group $minItems: 1 $itemName: Device or Partition ${i} $help: List of devices or partitions that belong to the volume $prototype: name: $name: Device or Partition $type: text $placeholder: /dev/sda1 $help: Device or Partition with type LVM bootloaderareasize: $name: Boot Loader Area Size $type: text $optional: yes $help: "Directly passed to 'pvcreate'" dataaligment: $name: Data Aligment $type: text $optional: yes $help: "Directly passed to 'pvcreate'" dataalignmentoffset: $name: Data Aligment Offset $type: text $optional: yes $help: "Directly passed to 'pvcreate'" volumes: $type: edit-group $minItems: 1 $itemName: Logical Volume ${i} $help: List of logical volumes $prototype: name: $name: Logical Volume Name $type: text $placeholder: root $help: Name of the logical volume extents: $type: text $optional: yes $placeholder: 100%FREE $help: "Directly passed to 'lvcreate'" size: $type: text $optional: yes $placeholder: 1024M $help: "Directly passed to 'lvcreate'" stripes: $type: number $optional: yes $help: "Directly passed to 'lvcreate'" stripesize: $name: Stripe Size $type: number $optional: yes $help: "Directly passed to 'lvcreate'" # There are more options that we can implement for LVM clustered: $type: select $optional: yes $values: - "y" - "n" $default: "n" $help: "Directly passed to 'vgcreate'" maxlogicalvolumes: $name: Max Logical Volumes $type: number $optional: yes $help: "Directly passed to 'vgcreate'" maxphysicalvolumes: $name: Max Physical Volumes $type: number $optional: yes $help: "Directly passed to 'vgcreate'" physicalextentsize: $name: Physical Extent Size $type: text $optional: yes $help: "Directly passed to 'vgcreate'" raid: $type: edit-group $minItems: 0 $itemName: RAID ${i} $help: RAID (Storage) Subsection for the Yomi Formula $prototype: $type: group $key: $name: RAID Device Name $type: text $placeholder: /dev/md0 $help: Name of the RAID device level: $type: select $values: - linear - raid0 - raid1 - mirror - raid4 - raid5 - raid6 - raid10 - multipath - faulty - container $default: raid1 $help: RAID type devices: $type: edit-group $minItems: 1 $itemName: Device or Partition ${i} $help: List of devices or partitions that belong to the RAID $prototype: name: $name: Device or Partition $type: text $placeholder: /dev/sda1 $help: Device or partition with type RAID metadata: $type: select $values: - 0 - 0.9 - 1 - 1.1 - 1.2 - default - ddm - imsm $default: default $help: RAID metadata version raid-devices: $type: number $optional: yes $help: Number of active devices in array spare-devices: $type: number $optional: yes $help: Number of spare (eXtra) devices in initial array filesystems: $type: edit-group $minItems: 1 $itemName: Filesystem ${i} $help: File System (Storage) Subsection for the Yomi Formula $prototype: $type: group $key: $name: Partition $type: text $placeholder: /dev/sda1 $help: Partition for the filesystem filesystem: $type: select $values: - swap - btrfs - xfs - ext2 - ext3 - ext4 - vfat $default: ext4 $help: Filesystem for the device mountpoint: $type: text $placeholder: / $visibleIf: .filesystem != swap $help: Mount point of the partition fat: $name: FAT Type $type: select $values: - 12 - 16 - 32 $visibleIf: .filesystem == vfat $help: Type of FAT subvolumes: $name: BtrFS Subvolumes $type: group $visibleIf: .filesystem == btrfs $help: List of Btrfs subvolumes prefix: $type: text $placeholder: '@' $help: Btrfs subvolume prefix subvolume: $type: edit-group $minItems: 0 $itemName: Subvolume ${i} $visibleIf: .prefix != "" $help: Subvolume description $prototype: path: $type: text $placeholder: /root $help: Path for the subvolume copy_on_write: $type: boolean $default: yes $help: CoW flag 07070100000012000081A40000000000000000000000016130D1CF00000480000000000000000000000000000000000000004700000000yomi-0.0.1+git.1630589391.4557cfd/metadata/03-form-yomi-bootloader.yml# Uyuni Form for the Yomi pillar data - Bootloader # # Find mode pillar examples in /usr/share/yomi/pillar bootloader: $type: group $help: Bootloader Section for the Yomi Formula device: $type: text $placeholder: /dev/sda $required: yes $help: Device where the GRUB2 will be installed timeout: $type: number $optional: yes $default: 8 $help: Value for the GRUB_TIMEOUT parameter kernel: $type: text $optional: yes $default: splash=silent quiet $help: Line assigned to the GRUB_CMDLINE_LINUX_DEFAULT parameter terminal: $type: text $optional: yes $default: gfxterm $help: Value for the GRUB_TERMINAL parameter serial_command: $type: text $optional: yes $help: Value for the GRUB_SERIAL_COMMAND parameter gfxmode: $type: text $optional: yes $default: auto $help: Value for the GRUB_GFXMODE parameter theme: $type: boolean $default: no $help: Install and configure grub2-branding package disable_os_prober: $name: Disable OS Prober $type: boolean $default: no $help: Value for the GRUB_DISABLE_OS_PROBER parameter 07070100000013000081A40000000000000000000000016130D1CF0000108F000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/metadata/04-form-yomi-software.yml# Uyuni Form for the Yomi pillar data - Software # # Find mode pillar examples in /usr/share/yomi/pillar software: $type: group $help: Software Section for the Yomi Formula config: $name: Configuration $type: group $help: Local configuration for the software section minimal: $type: boolean $default: no $help: Exclude recommended, documentation and multi-version packages transfer: $type: boolean $default: no $help: Transfer the current repositories from the media verify: $type: boolean $default: yes $help: Verify the package key when installing enabled: $type: boolean $default: yes $help: Enable the repository refresh: $type: boolean $default: yes $help: Enable auto-refresh of the repository gpgcheck: $type: boolean $default: yes $help: Enable the GPG check for the repositories # gpgautoimport: # $type: boolean # $default: yes # $help: Automatically trust and import public GPG key cache: $type: boolean $default: no $help: Keep the RPM packages in the system repositories: $type: edit-group $minItems: 0 $itemName: Repository ${i} $help: List of registered repositories $prototype: $type: group $key: $name: Alias $type: text $placeholder: repo-oss $help: Short name or alias of the repository url: $type: url $placeholder: http://download.opensuse.org/tumbleweed/repo/oss $required: yes $help: URL of the repository name: $type: text $optional: yes $help: Descriptive name for the repository enabled: $type: boolean $default: yes $help: Enable the repository refresh: $type: boolean $default: yes $help: Enable auto-refresh of the repository priority: $type: number $help: Set priority of the repository gpgcheck: $type: boolean $default: yes $help: Enable the GPG check for the repositories # gpgautoimport: # $type: boolean # $default: yes # $help: Automatically trust and import public GPG key cache: $type: boolean $default: no $help: Keep the RPM packages in the system packages: $type: edit-group $minItems: 0 $itemName: Package ${i} $help: List of patterns or packages to install $prototype: $name: Package $type: text $help: "You can install patterns using the 'pattern:' prefix" image: $type: group $optional: yes $help: Image ISO used to dump in the hard disk url: $name: Image URL $type: url $help: URL from where download the image md5: $type: text $optional: yes $help: MD5 of the image, used for validation suseconnect: $name: SUSEConnect $type: group $help: SUSEConnect Section for the Yomi Formula config: $type: group $help: Local configuration for the section regcode: $name: Registration Code $type: text $help: Subscription registration code for the product email: $type: text $optional: yes $help: Email address for product registration url: $type: url $optional: yes $placeholder: https://scc.suse.com $help: URL of registration server version: $type: text $optional: yes $help: Version part of the product name arch: $name: Architecture $type: text $optional: yes $help: Architecture part of the product name products: $type: edit-group $minItems: 0 $itemName: Product ${i} $help: List of products to register $prototype: $type: text $placeholder: <name>/<version>/<architecture> $help: The expected format is <name>/<version>/<architecture> packages: $type: edit-group $minItems: 0 $itemName: Package ${i} $help: List of patterns or packages to install from the products $prototype: $name: Package $type: text $help: "You can install patterns using the 'pattern:' prefix" 07070100000014000081A40000000000000000000000016130D1CF00000362000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/metadata/05-form-yomi-services.yml# Uyuni Form for the Yomi pillar data - Services # # Find mode pillar examples in /usr/share/yomi/pillar salt-minion: $type: group $help: Salt Minion Section for the Yomi Formula config: $name: Install salt-minion $type: boolean $default: yes $help: (Provisional) Install and configure a salt-minion service services: $type: group $help: Service Section for the Yomi Formula enabled: $type: edit-group $minItems: 0 $itemName: Service ${i} $help: List of enabled services $prototype: $key: $type: text $name: Service $help: Name of the service to enable disabled: $type: edit-group $minItems: 0 $itemName: Service ${i} $help: List of disabled services $prototype: $key: $type: text $name: Service $help: Name of the service to disable 07070100000015000081A40000000000000000000000016130D1CF000002CC000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/metadata/06-form-yomi-users.yml# Uyuni Form for the Yomi pillar data - Users # # Find mode pillar examples in /usr/share/yomi/pillar users: $type: edit-group $minItems: 1 $itemName: User ${i} $help: List of users of the system $prototype: username: $type: text password: $name: Password Hash $type: text $help: "You can generate a hash with 'openssl passwd -1 -salt <salt> <password>'" certificates: $type: edit-group $minItems: 0 $itemName: Certificate ${i} $prototype: $key: $name: Certificate $type: text $help: "Will be added to .ssh/authorized_keys. Use only the encoded key (remove the 'ssh-rsa' prefix and the 'user@host' suffix)" 07070100000016000081A40000000000000000000000016130D1CF0000003E000000000000000000000000000000000000003800000000yomi-0.0.1+git.1630589391.4557cfd/metadata/metadata.ymldescription: Yet one more installer group: installer #AFTER 07070100000017000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000002900000000yomi-0.0.1+git.1630589391.4557cfd/pillar070701000000180000A1FF000000000000000000000001611CDAFF00000013000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.image_storage.sls.single070701000000190000A1FF000000000000000000000001611CDAFF00000014000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.kubic_storage.sls.microos0707010000001A000081A40000000000000000000000016130D1CF000009C6000000000000000000000000000000000000003A00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.lvm# # Storage section for a LVM with three devices deployment # partitions: config: label: {{ partition }} # Same gap for all devices initial_gap: 1MB devices: /dev/{{ device_type }}a: partitions: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} - number: {{ next_partition }} size: 1MB type: boot {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} - number: {{ next_partition }} size: 256MB type: efi {% set next_partition = next_partition + 1 %} {% endif %} - number: {{ next_partition }} size: rest type: lvm /dev/{{ device_type }}b: partitions: - number: 1 size: rest type: lvm /dev/{{ device_type }}c: partitions: - number: 1 size: rest type: lvm lvm: system: devices: - /dev/{{ device_type }}a{{ 2 if efi else 1 }} - /dev/{{ device_type }}b1 - name: /dev/{{ device_type }}c1 dataalignmentoffset: 7s clustered: 'n' volumes: {% if swap %} - name: swap size: 1024M {% endif %} - name: root {% if home_filesystem %} size: 16384M {% else %} extents: 100%FREE {% endif %} {% if home_filesystem %} - name: home extents: 100%FREE {% endif %} filesystems: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: vfat mountpoint: /boot/efi {% set next_partition = next_partition + 1 %} {% endif %} /dev/system/swap: filesystem: swap /dev/system/root: filesystem: {{ root_filesystem }} mountpoint: / {% if root_filesystem == 'btrfs' %} subvolumes: prefix: '@' subvolume: {% if not home_filesystem %} - path: home {% endif %} - path: opt - path: root - path: srv - path: tmp - path: usr/local - path: var copy_on_write: no {% if arch == 'aarch64' %} - path: boot/grub2/arm64-efi {% else %} - path: boot/grub2/i386-pc - path: boot/grub2/x86_64-efi {% endif %} {% endif %} {% if home_filesystem %} /dev/system/home: filesystem: {{ home_filesystem }} mountpoint: /home {% endif %} bootloader: device: /dev/{{ device_type }}a theme: yes 0707010000001B000081A40000000000000000000000016130D1CF00000926000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.microos# # Storage section for a microos deployment in a single device # {% if swap %} {{ raise ('Do not define a SWAP partition for MicoOS') }} {% endif %} {% if home_filesystem %} {{ raise ('Do not define a separate home partition for MicoOS') }} {% endif %} {% if root_filesystem != 'btrfs' %} {{ raise ('File system must be BtrFS for MicoOS') }} {% endif %} {% if not snapper %} {{ raise ('Snapper is required for MicoOS') }} {% endif %} partitions: config: label: {{ partition }} devices: /dev/{{ device_type }}a: initial_gap: 1MB partitions: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} - number: {{ next_partition }} size: 1MB type: boot {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} - number: {{ next_partition }} size: 256MB type: efi {% set next_partition = next_partition + 1 %} {% endif %} - number: {{ next_partition }} size: 16384MB type: linux {% set next_partition = next_partition + 1 %} - number: {{ next_partition }} size: rest type: linux {% set next_partition = next_partition + 1 %} filesystems: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: vfat mountpoint: /boot/efi {% set next_partition = next_partition + 1 %} {% endif %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: {{ root_filesystem }} mountpoint: / options: [ro] subvolumes: prefix: '@' subvolume: - path: root - path: home - path: opt - path: srv - path: boot/writable - path: usr/local {% if arch == 'aarch64' %} - path: boot/grub2/arm64-efi {% else %} - path: boot/grub2/i386-pc - path: boot/grub2/x86_64-efi {% endif %} {% set next_partition = next_partition + 1 %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: {{ root_filesystem }} mountpoint: /var {% set next_partition = next_partition + 1 %} bootloader: device: /dev/{{ device_type }}a kernel: swapaccount=1 disable_os_prober: yes theme: yes 0707010000001C000081A40000000000000000000000016130D1CF00000B49000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.raid1# # Storage section for a RAID 1 with three devices deployment # partitions: config: label: gpt # Same gap for all devices initial_gap: 1MB devices: /dev/{{ device_type }}a: partitions: - number: 1 size: rest type: raid /dev/{{ device_type }}b: partitions: - number: 1 size: rest type: raid /dev/{{ device_type }}c: partitions: - number: 1 size: rest type: raid /dev/md0: partitions: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} - number: {{ next_partition }} size: 1MB type: boot {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} - number: {{ next_partition }} size: 256MB type: efi {% set next_partition = next_partition + 1 %} {% endif %} {% if swap %} - number: {{ next_partition }} size: 1024MB type: swap {% set next_partition = next_partition + 1 %} {% endif %} - number: {{ next_partition }} size: {{ 'rest' if not home_filesystem else '16384MB' }} type: linux {% set next_partition = next_partition + 1 %} {% if home_filesystem %} - number: {{ next_partition }} size: rest type: linux {% set next_partition = next_partition + 1 %} {% endif %} raid: /dev/md0: level: 1 devices: - /dev/{{ device_type }}a1 - /dev/{{ device_type }}b1 - /dev/{{ device_type }}c1 spare-devices: 1 metadata: 1.0 filesystems: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} /dev/md0p{{ next_partition }}: filesystem: vfat mountpoint: /boot/efi {% set next_partition = next_partition + 1 %} {% endif %} {% if swap %} /dev/md0p{{ next_partition }}: filesystem: swap {% set next_partition = next_partition + 1 %} {% endif %} /dev/md0p{{ next_partition }}: filesystem: {{ root_filesystem }} mountpoint: / {% if root_filesystem == 'btrfs' %} subvolumes: prefix: '@' subvolume: {% if not home_filesystem %} - path: home {% endif %} - path: opt - path: root - path: srv - path: tmp - path: usr/local - path: var copy_on_write: no {% if arch == 'aarch64' %} - path: boot/grub2/arm64-efi {% else %} - path: boot/grub2/i386-pc - path: boot/grub2/x86_64-efi {% endif %} {% endif %} {% set next_partition = next_partition + 1 %} {% if home_filesystem %} /dev/md0p{{ next_partition }}: filesystem: {{ home_filesystem }} mountpoint: /home {% set next_partition = next_partition + 1 %} {% endif %} bootloader: device: /dev/md0 theme: yes 0707010000001D000081A40000000000000000000000016130D1CF00000984000000000000000000000000000000000000003D00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.single# # Storage section for a single device deployment # partitions: config: label: {{ partition }} devices: /dev/{{ device_type }}a: initial_gap: 1MB partitions: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} - number: {{ next_partition }} size: 1MB type: boot {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} - number: {{ next_partition }} size: 256MB type: efi {% set next_partition = next_partition + 1 %} {% endif %} {% if swap %} - number: {{ next_partition }} size: 1024MB type: swap {% set next_partition = next_partition + 1 %} {% endif %} - number: {{ next_partition }} size: {{ 'rest' if not home_filesystem else '16384MB' }} type: linux {% set next_partition = next_partition + 1 %} {% if home_filesystem %} - number: {{ next_partition }} size: rest type: linux {% set next_partition = next_partition + 1 %} {% endif %} filesystems: {% set next_partition = 1 %} {% if not efi and partition == 'gpt' %} {% set next_partition = next_partition + 1 %} {% endif %} {% if efi and partition == 'gpt' %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: vfat mountpoint: /boot/efi {% set next_partition = next_partition + 1 %} {% endif %} {% if swap %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: swap {% set next_partition = next_partition + 1 %} {% endif %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: {{ root_filesystem }} mountpoint: / {% if root_filesystem == 'btrfs' %} subvolumes: prefix: '@' subvolume: {% if not home_filesystem %} - path: home {% endif %} - path: opt - path: root - path: srv - path: tmp - path: usr/local - path: var copy_on_write: no {% if arch == 'aarch64' %} - path: boot/grub2/arm64-efi {% else %} - path: boot/grub2/i386-pc - path: boot/grub2/x86_64-efi {% endif %} {% endif %} {% set next_partition = next_partition + 1 %} {% if home_filesystem %} /dev/{{ device_type }}a{{ next_partition }}: filesystem: {{ home_filesystem }} mountpoint: /home {% set next_partition = next_partition + 1 %} {% endif %} bootloader: device: /dev/{{ device_type }}a theme: yes 0707010000001E0000A1FF000000000000000000000001611CDAFF00000013000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/pillar/_storage.sls.sles_storage.sls.single0707010000001F000081A40000000000000000000000016130D1CF00001006000000000000000000000000000000000000003700000000yomi-0.0.1+git.1630589391.4557cfd/pillar/installer.sls# Meta pillar for testing Yomi # # There are some parameters that can be configured and adapted to # launch a basic Yomi installation: # # * efi = {True, False} # * partition = {'msdos', 'gpt'} # * device_type = {'sd', 'hd', 'vd'} # * root_filesystem = {'ext{2, 3, 4}', 'btrfs'} # * home_filesystem = {'ext{2, 3, 4}', 'xfs', False} # * snapper = {True, False} # * swap = {True, False} # * mode = {'single', 'lvm', 'raid{0, 1, 4, 5, 6, 10}', 'microos', # 'kubic', 'image', 'sles'} # * network = {'auto', 'eth0', 'ens3', ... } # # This meta-pillar can be used as a template for new installers. This # template is expected to be adapted for production systems, as was # designed for CI / QA and development. # We cannot access to grains['efi'] from the pillar, as this is not # yet synchronized {% set efi = True %} {% set partition = 'gpt' %} {% set device_type = 'sd' %} {% set root_filesystem = 'btrfs' %} {% set home_filesystem = False %} {% set snapper = True %} {% set swap = False %} {% set mode = 'microos' %} {% set network = 'auto' %} {% set arch = grains['cpuarch'] %} config: events: no reboot: no {% if snapper and root_filesystem == 'btrfs' %} snapper: yes {% endif %} locale: en_US.UTF-8 keymap: us timezone: UTC hostname: node {% include "_storage.sls.%s" % mode %} {% if mode == 'sles' %} suseconnect: config: regcode: INTERNAL-USE-ONLY-f7fe-e9d9 version: '15.2' arch: {{ arch }} products: - sle-module-basesystem - sle-module-server-applications {% endif %} software: config: minimal: {{ 'yes' if mode in ('microos', 'kubic') else 'no' }} enabled: yes autorefresh: yes gpgcheck: yes repositories: {% if mode == 'sles' %} SUSE_SLE-15_GA: "http://download.suse.de/ibs/SUSE:/SLE-15:/GA/standard/" SUSE_SLE-15_Update: "http://download.suse.de/ibs/SUSE:/SLE-15:/Update/standard/" SUSE_SLE-15-SP1_GA: "http://download.suse.de/ibs/SUSE:/SLE-15-SP1:/GA/standard/" SUSE_SLE-15-SP1_Update: "http://download.suse.de/ibs/SUSE:/SLE-15-SP1:/Update/standard/" SUSE_SLE-15-SP2_GA: "http://download.suse.de/ibs/SUSE:/SLE-15-SP2:/GA/standard/" SUSE_SLE-15-SP2_Update: "http://download.suse.de/ibs/SUSE:/SLE-15-SP2:/Update/standard/" {% elif arch == 'aarch64' %} repo-oss: "http://download.opensuse.org/ports/aarch64/tumbleweed/repo/oss/" {% else %} repo-oss: url: "http://download.opensuse.org/tumbleweed/repo/oss/" name: openSUSE-Tumbleweed {% endif %} {% if mode == 'image' %} image: url: tftp://10.0.3.1/openSUSE-Tumbleweed-Yomi{{ arch }}-1.0.0.xz md5: {% else %} packages: {% if mode == 'microos' %} - pattern:microos_base - pattern:microos_defaults - pattern:microos_hardware {% elif mode == 'kubic' %} - pattern:microos_base - pattern:microos_defaults - pattern:microos_hardware - pattern:microos_apparmor - pattern:kubic_worker {% elif mode == 'sles' %} - product:SLES - pattern:base - pattern:enhanced_base - pattern:yast2_basis - pattern:x11_yast - pattern:x11 - pattern:gnome_basic {% else %} - pattern:enhanced_base - glibc-locale {% endif %} - kernel-default {% endif %} salt-minion: config: yes services: enabled: {% if mode == 'kubic' %} - crio - kubelet {% endif %} - salt-minion {% if network != 'auto' %} networks: - interface: {{ network }} {% endif %} users: - username: root # Set the password as 'linux'. Do not do that in production password: "$1$wYJUgpM5$RXMMeASDc035eX.NbYWFl0" # Personal certificate, without the type prefix nor the host # suffix certificates: - "AAAAB3NzaC1yc2EAAAADAQABAAABAQDdP6oez825gnOLVZu70KqJXpqL4fGf\ aFNk87GSk3xLRjixGtr013+hcN03ZRKU0/2S7J0T/dICc2dhG9xAqa/A31Qac\ hQeg2RhPxM2SL+wgzx0geDmf6XDhhe8reos5jgzw6Pq59gyWfurlZaMEZAoOY\ kfNb5OG4vQQN8Z7hldx+DBANPbylApurVz6h5vvRrkPfuRVN5ZxOkI+LeWhpo\ vX5XK3eTjetAwWEro6AAXpGoQQQDjSOoYHCUmXzcZkmIWEubCZvAI4RZ+XCZs\ +wTeO2RIRsunqP8J+XW4cZ28RZBc9K4I1BV8C6wBxN328LRQcilzw+Me+Lfre\ eDPglqx" 07070100000020000081A40000000000000000000000016130D1CF0000001D000000000000000000000000000000000000003100000000yomi-0.0.1+git.1630589391.4557cfd/pillar/top.slsbase: '*': - installer 07070100000021000041ED0000000000000000000000066130D1CF00000000000000000000000000000000000000000000002700000000yomi-0.0.1+git.1630589391.4557cfd/salt07070100000022000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003000000000yomi-0.0.1+git.1630589391.4557cfd/salt/_modules07070100000023000081A40000000000000000000000016130D1CF00002BE7000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_modules/devices.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging LOG = logging.getLogger(__name__) __virtualname__ = "devices" __func_alias__ = { "filter_": "filter", } # Define not exported variables from Salt, so this can be imported as # a normal module try: __grains__ __salt__ except NameError: __grains__ = {} __salt__ = {} def _udev(udev_info, key): """ Return the value for a udev key. The `key` parameter is a lower case text joined by dots. For example, 'e.id_bus' will represent the key for `udev_info['E']['ID_BUS']`. """ k, _, r = key.partition(".") if not k: return udev_info if not isinstance(udev_info, dict): return "n/a" if not r: return udev_info.get(k.upper(), "n/a") return _udev(udev_info.get(k.upper(), {}), r) def _match(udev_info, match_info): """ Check if `udev_info` match the information from `match_info`. """ res = True for key, value in match_info.items(): udev_value = _udev(udev_info, key) if isinstance(udev_value, dict): # If is a dict we probably make a mistake in key from # match_info, as is not accessing a final value LOG.warning( "The key %s for the udev information " "dictionary is not a leaf element", key, ) continue # Converting both values to sets make easy to see if there is # a coincidence between both values value = set(value) if isinstance(value, list) else set([value]) udev_value = ( set(udev_value) if isinstance(udev_value, list) else set([udev_value]) ) res = res and (value & udev_value) return res def filter_(udev_in=None, udev_ex=None): """ Returns a list of devices, filtered under udev keys. udev_in A dictionary of key:values that are expected in the device udev information udev_ex A dictionary of key:values that are not expected in the device udev information (excluded) The key is a lower case string, joined by dots, that represent a path in the udev information dictionary. For example, 'e.id_bus' will represent the udev entry `udev['E']['ID_BUS'] If the udev entry is a list, the algorithm will check that at least one item match one item of the value of the parameters. Returns list of devices that match `udev_in` and do not match `udev_ex`. CLI Example: .. code-block:: bash salt '*' devices.filter udev_in='{"e.id_bus": "ata"}' """ udev_in = udev_in if udev_in else {} udev_ex = udev_ex if udev_ex else {} all_devices = __grains__["disks"] # Get the udev information only one time udev_info = {d: __salt__["udev.info"](d) for d in all_devices} devices_udev_key_in = {d for d in all_devices if _match(udev_info[d], udev_in)} devices_udev_key_ex = { d for d in all_devices if _match(udev_info[d], udev_ex) if udev_ex } return sorted(devices_udev_key_in - devices_udev_key_ex) def wipe(device): """ Remove all the partitions in the device. device Device name, for example /dev/sda Remove all the partitions, labels and flags from the device. CLI Example: .. code-block:: bash salt '*' devices.wipe /dev/sda """ partitions = __salt__["partition.list"](device).get("partitions", []) for partition in partitions: # Remove filesystem information the the partition __salt__["disk.wipe"]("{}{}".format(device, partition)) __salt__["partition.rm"](device, partition) # Remove the MBR information __salt__["disk.wipe"]("{}".format(device)) __salt__["cmd.run"]("dd bs=512 count=1 if=/dev/zero of={}".format(device)) return True def _hwinfo_parse_short(report): """Parse the output of hwinfo and return a dictionary""" result = {} current_result = {} key_counter = 0 for line in report.strip().splitlines(): if line.startswith(" "): key = key_counter key_counter += 1 current_result[key] = line.strip() elif line.startswith(" "): key, value = line.strip().split(" ", 1) current_result[key] = value.strip() elif line.endswith(":"): key = line[:-1] value = {} result[key] = value current_result = value key_counter = 0 else: LOG.error("Error parsing hwinfo short output: {}".format(line)) return result def _hwinfo_parse_full(report): """Parse the output of hwinfo and return a dictionary""" result = {} result_stack = [] level = 0 for line in report.strip().splitlines(): current_level = line.count(" ") if level != current_level or len(result_stack) != result_stack: result_stack = result_stack[:current_level] level = current_level line = line.strip() # Ignore empty lines if not line: continue # Initial line of a segment if level == 0: key, value = line.split(":", 1) sub_result = {} result[key] = sub_result # The first line contains also a sub-element key, value = value.strip().split(": ", 1) sub_result[key] = value result_stack.append(sub_result) level += 1 continue # Line is a note if line.startswith("[") or ":" not in line: sub_result = result_stack[-1] sub_result["Note"] = line if not line.startswith("[") else line[1:-1] continue key, value = line.split(":", 1) key, value = key.strip(), value.strip() sub_result = result_stack[-1] # If there is a value and it not starts with hash, this is a # (key, value) entry. But there are exception on the rule, # like when is about 'El Torito info', that is the begining of # a new dictorionart. if value and not value.startswith("#") and key != "El Torito info": if key == "I/O Port": key = "I/O Ports" elif key == "Config Status": value = dict(item.split("=") for item in value.split(", ")) elif key in ("Driver", "Driver Modules"): value = value.replace('"', "").split(", ") elif key in ("Tags", "Device Files", "Features"): # We cannot split by ', ', as using spaces in # inconsisten in some fields value = [v.strip() for v in value.split(",")] else: if value.startswith('"'): value = value[1:-1] # If there is a collision, we store it as a list if key in sub_result: current_value = sub_result[key] if type(current_value) is not list: current_value = [current_value] if value not in current_value: current_value.append(value) if len(current_value) == 1: value = current_value[0] else: value = current_value sub_result[key] = value else: if value.startswith("#"): value = {"Handle": value} elif key == "El Torito info": value = value.split(", ") value = { "platform": value[0].split()[-1], "bootable": "no" if "not" in value[1] else "yes", } else: value = {} sub_result[key] = value result_stack.append(value) level += 1 return result def _hwinfo_parse(report, short): """Parse the output of hwinfo and return a dictionary""" if short: return _hwinfo_parse_short(report) else: return _hwinfo_parse_full(report) def _hwinfo_efi(): """Return information about EFI""" return { "efi": __grains__["efi"], "efi-secure-boot": __grains__["efi-secure-boot"], } def _hwinfo_memory(): """Return information about the memory""" return { "mem_total": __grains__["mem_total"], } def _hwinfo_network(short): """Return network information""" info = { "fqdn": __grains__["fqdn"], "ip_interfaces": __grains__["ip_interfaces"], } if not short: info["dns"] = __grains__["dns"] return info def hwinfo(items=None, short=True, listmd=False, devices=None): """ Probe for hardware items List of hardware items to inspect. Default ['bios', 'cpu', 'disk', 'memory', 'network', 'partition'] short Show only a summary. Default True. listmd Report RAID devices. Default False. devices List of devices to show information from. Default None. CLI Example: .. code-block:: bash salt '*' devices.hwinfo salt '*' devices.hwinfo items='["disk"]' short=no salt '*' devices.hwinfo items='["disk"]' short=no devices='["/dev/sda"]' salt '*' devices.hwinfo devices=/dev/sda """ result = {} if not items: items = ["bios", "cpu", "disk", "memory", "network", "partition"] if not isinstance(items, (list, tuple)): items = [items] if not devices: devices = [] if devices and not isinstance(devices, (list, tuple)): devices = [devices] cmd = ["hwinfo"] for item in items: cmd.append("--{}".format(item)) if short: cmd.append("--short") if listmd: cmd.append("--listmd") for device in devices: cmd.append("--only {}".format(device)) out = __salt__["cmd.run_stdout"](cmd) result["hwinfo"] = _hwinfo_parse(out, short) if "bios" in items: result["bios grains"] = _hwinfo_efi() if "memory" in items: result["memory grains"] = _hwinfo_memory() if "network" in items: result["network grains"] = _hwinfo_network(short) return result 07070100000024000081A40000000000000000000000016130D1CF00000703000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_modules/filters.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging LOG = logging.getLogger(__name__) __virtualname__ = "filters" # Define not exported variables from Salt, so this can be imported as # a normal module try: __pillar__ except NameError: __pillar__ = {} def is_lvm(device): """Detect if a device name comes from a LVM volume.""" devices = ["/dev/{}/".format(i) for i in __pillar__.get("lvm", {})] devices.extend(("/dev/mapper/", "/dev/dm-")) return device.startswith(tuple(devices)) def is_raid(device): """Detect if a device name comes from a RAID array.""" return device.startswith("/dev/md") def is_not_raid(device): """Detect if a device name comes from a RAID array.""" return not is_raid(device) 07070100000025000081A40000000000000000000000016130D1CF00001F5D000000000000000000000000000000000000003A00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_modules/images.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging import pathlib import urllib.parse from salt.exceptions import SaltInvocationError, CommandExecutionError import salt.utils.args LOG = logging.getLogger(__name__) __virtualname__ = "images" # Define not exported variables from Salt, so this can be imported as # a normal module try: __salt__ except NameError: __salt__ = {} VALID_SCHEME = ( "dict", "file", "ftp", "ftps", "gopher", "http", "https", "imap", "imaps", "ldap", "ldaps", "pop3", "pop3s", "rtmp", "rtsp", "scp", "sftp", "smb", "smbs", "smtp", "smtps", "telnet", "tftp", ) VALID_COMPRESSIONS = ("gz", "bz2", "xz") VALID_CHECKSUMS = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512") def _checksum_url(url, checksum_type): """Generate the URL for the checksum""" url_elements = urllib.parse.urlparse(url) path = url_elements.path suffix = pathlib.Path(path).suffix new_suffix = ".{}".format(checksum_type) if suffix[1:] in VALID_COMPRESSIONS: path = pathlib.Path(path).with_suffix(new_suffix) else: path = pathlib.Path(path).with_suffix(suffix + new_suffix) return urllib.parse.urlunparse(url_elements._replace(path=str(path))) def _curl_cmd(url, **kwargs): """Return curl commmand line""" cmd = ["curl"] for key, value in salt.utils.args.clean_kwargs(**kwargs).items(): if len(key) == 1: cmd.append("-{}".format(key)) else: cmd.append("--{}".format(key)) if value is not None: cmd.append(value) cmd.append(url) return cmd def _fetch_file(url, **kwargs): """Get a file and return the content""" params = { "silent": None, "location": None, } params.update(kwargs) return __salt__["cmd.run_stdout"](_curl_cmd(url, **params)) def _find_filesystem(device): """Use lsblk to find the filesystem of a partition.""" cmd = ["lsblk", "--noheadings", "--output", "FSTYPE", device] return __salt__["cmd.run_stdout"](cmd) def fetch_checksum(url, checksum_type, **kwargs): """ Fecht the checksum from an image URL url URL of the image. The protocol scheme needs to be available in curl. For example: http, https, scp, sftp, tftp or ftp. The image can be compressed, and the supported extensions are: gz, bz2 and xz checksum_type The type of checksum used to validate the image, possible values are 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'. Other paramaters send via kwargs will be used during the call for curl. CLI Example: .. code-block:: bash salt '*' images.fetch_checksum https://my.url/JeOS.xz checksum_type=md5 """ checksum_url = _checksum_url(url, checksum_type) checksum = _fetch_file(checksum_url, **kwargs) if not checksum: raise CommandExecutionError( "Checksum file not found in {}".format(checksum_url) ) checksum = checksum.split()[0] LOG.info("Checksum for the image {}".format(checksum)) return checksum def dump(url, device, checksum_type=None, checksum=None, **kwargs): """Download an image and copy it into a device url URL of the image. The protocol scheme needs to be available in curl. For example: http, https, scp, sftp, tftp or ftp. The image can be compressed, and the supported extensions are: gz, bz2 and xz device The device or partition where the image will be copied. checksum_type The type of checksum used to validate the image, possible values are 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'. checksum The checksum value. If omitted but a `checksum_type` was set, it will try to download the checksum file from the same URL, replacing the extension with the `checksum_type` Other paramaters send via kwargs will be used during the call for curl. If succeed it will return the real checksum of the image. If checksum_type is not specified, MD5 will be used. CLI Example: .. code-block:: bash salt '*' images.dump https://my.url/JeOS-btrfs.xz /dev/sda1 salt '*' images.dump tftp://my.url/JeOS.xz /dev/sda1 checksum_type=md5 """ scheme, _, path, *_ = urllib.parse.urlparse(url) if scheme not in VALID_SCHEME: raise SaltInvocationError("Protocol not valid for URL") # We cannot validate the compression extension, as we can have # non-restricted file names, like '/my-image.ext3' or # 'other-image.raw'. if checksum_type and checksum_type not in VALID_CHECKSUMS: raise SaltInvocationError("Checksum type not valid") if not checksum_type and checksum: raise SaltInvocationError("Checksum type not provided") if checksum_type and not checksum: checksum = fetch_checksum(url, checksum_type, **kwargs) params = { "fail": None, "location": None, "silent": None, } params.update(kwargs) # If any element in the pipe fail, exit early cmd = ["set -eo pipefail", ";"] cmd.extend(_curl_cmd(url, **params)) suffix = pathlib.Path(path).suffix[1:] if suffix in VALID_COMPRESSIONS: cmd.append("|") cmd.extend( {"gz": ["gunzip"], "bz2": ["bzip2", "-d"], "xz": ["xz", "-d"]}[suffix] ) checksum_prg = "{}sum".format(checksum_type) if checksum_type else "md5sum" cmd.extend(["|", "tee", device, "|", checksum_prg]) ret = __salt__["cmd.run_all"](" ".join(cmd), python_shell=True) if ret["retcode"]: raise CommandExecutionError( "Error while fetching image {}: {}".format(url, ret["stderr"]) ) new_checksum = ret["stdout"].split()[0] if checksum_type and checksum != new_checksum: raise CommandExecutionError( "Checksum mismatch. " "Expected {}, calculated {}".format(checksum, new_checksum) ) filesystem = _find_filesystem(device) resize_cmd = { "ext2": "e2fsck -f -y {0}; resize2fs {0}".format(device), "ext3": "e2fsck -f -y {0}; resize2fs {0}".format(device), "ext4": "e2fsck -f -y {0}; resize2fs {0}".format(device), "btrfs": "mount {} /mnt; btrfs filesystem resize max /mnt;" " umount /mnt".format(device), "xfs": "mount {} /mnt; xfs_growfs /mnt; umount /mnt".format(device), } if filesystem not in resize_cmd: raise CommandExecutionError( "Filesystem {} cannot be resized.".format(filesystem) ) ret = __salt__["cmd.run_all"](resize_cmd[filesystem], python_shell=True) if ret["retcode"]: raise CommandExecutionError( "Error while resizing the partition {}: {}".format(device, ret["stderr"]) ) __salt__["cmd.run"]("sync") return new_checksum 07070100000026000081A40000000000000000000000016130D1CF00002BAF000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_modules/partmod.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging from salt.exceptions import SaltInvocationError import lp import disk LOG = logging.getLogger(__name__) __virtualname__ = "partmod" # Define not exported variables from Salt, so this can be imported as # a normal module try: __grains__ __salt__ except NameError: __grains__ = {} __salt__ = {} PENALIZATION = { # Penalization for wasted space remaining in the device "free": 1, # Default penalizations "minimum_recommendation_size": 5, "maximum_recommendation_size": 2, "decrement_current_partition_size": 10, "increment_current_partition_size": 10, # / "root_minimum_recommendation_size": 5, "root_maximum_recommendation_size": 2, "root_decrement_current_partition_size": 10, "root_increment_current_partition_size": 10, # /home "home_minimum_recommendation_size": 5, "home_maximum_recommendation_size": 2, "home_decrement_current_partition_size": 10, "home_increment_current_partition_size": 10, # /var "var_minimum_recommendation_size": 5, "var_maximum_recommendation_size": 2, "var_decrement_current_partition_size": 10, "var_increment_current_partition_size": 10, } FREE = "free" MIN = "minimum_recommendation_size" MAX = "maximum_recommendation_size" INC = "decrement_current_partition_size" DEC = "increment_current_partition_size" # Default values for some partition parameters LABEL = "msdos" INITIAL_GAP = 0 UNITS = "MB" VALID_PART_TYPE = ("swap", "linux", "boot", "efi", "lvm", "raid") def _penalization(partition=None, section=FREE): """Penalization for a partition.""" kind = "{}_{}".format(partition, section) if kind in PENALIZATION: return PENALIZATION[kind] return PENALIZATION[section] def plan(name, constraints, unit="MB", export=False): """Analyze the current hardware and make a partition proposal. name Name of the root element of the dictionary constraints List of constraints for the partitions. Each element of the list will be a tuple with a name of partition, aminimum size (None if not required), and a maximum size (None if not required). Example: "[['swap', null, null], ['home', 524288, null]]" unit Unit where the sizes are expressed. Are the same valid units for the parted module export Export the partition proposal as a grains under the given name CLI Example: .. code-block:: bash salt '*' pplan.plan proposal "[['swap', null, null], ...]" """ if not constraints: raise SaltInvocationError("contraints parameter is required") hd_size = __salt__["status.diskusage"]("/dev/sda")["/dev/sda"]["total"] # TODO(aplanas) We only work on MB hd_size /= 1024 # TODO(aplanas) Fix the situation with swap. # Replace the None in the max position in the constraints with # hd_size. constraints = [(c[0], c[1], c[2] if c[2] else hd_size) for c in constraints] # Generate the variables of our model: # <part>_size, <part>_to_min_size, <part>_from_max_size variables = [ "{}_{}".format(constraint[0], suffix) for constraint in constraints for suffix in ("size", "to_min_size", "from_max_size") ] model = lp.Model(variables) for constraint in constraints: part_size = "{}_size".format(constraint[0]) part_to_min_size = "{}_to_min_size".format(constraint[0]) part_from_max_size = "{}_from_max_size".format(constraint[0]) model_constraints = ( # <part>_size >= MINIMUM_RECOMMENDATION_SIZE - <part>_to_min_size ({part_size: 1, part_to_min_size: 1}, lp.GTE, constraint[1]), # <part>_size <= MAXIMUM_RECOMMENDATION_SIZE + <part>_from_max_size ({part_size: 1, part_from_max_size: 1}, lp.LTE, constraint[2]), ) for model_constraint in model_constraints: model.add_constraint_named(*model_constraint) # sum(<part>_size) <= HD_SIZE model_constraint = ( {"{}_size".format(c[0]): 1 for c in constraints}, lp.LTE, hd_size, ) model.add_constraint_named(*model_constraint) # Minimize: PENALIZATION_FREE * (HD_SIZE - Sum(<part>_size)) # + PENALIZATION_MINIMUM_RECOMMENDATION_SIZE * <part>_to_min_size # + PENALIZATION_MAXIMUM_RECOMMENDATIOM_SIZE * <part>_from_max_size coefficients = { "{}_{}".format(constraint[0], suffix): _penalization( partition=constraint[0], section={"to_min_size": MIN, "from_max_size": MAX}[suffix], ) for constraint in constraints for suffix in ("to_min_size", "from_max_size") } coefficients.update( { "{}_size".format(constraint[0]): -_penalization(section=FREE) for constraint in constraints } ) model.add_cost_function_named( lp.MINIMIZE, coefficients, _penalization(section=FREE) * hd_size ) plan = {name: model.simplex()} if export: __salt__["grains.setvals"](plan) return plan def prepare_partition_data(partitions): """Helper function to prepare the patition data from the pillar.""" # Validate and normalize the `partitions` pillar. The state will # expect a dictionary with this schema: # # partitions_normalized = { # '/dev/sda': { # 'label': 'gpt', # 'pmbr_boot': False, # 'partitions': [ # { # 'part_id': '/dev/sda1', # 'part_type': 'primary' # 'fs_type': 'ext2', # 'flags': ['esp'], # 'start': '0MB', # 'end': '100%', # }, # ], # }, # } is_uefi = __grains__["efi"] # Get the fallback values for label and initial_gap config = partitions.get("config", {}) global_label = config.get("label", LABEL) global_initial_gap = config.get("initial_gap", INITIAL_GAP) partitions_normalized = {} for device, device_info in partitions["devices"].items(): label = device_info.get("label", global_label) initial_gap = device_info.get("initial_gap", global_initial_gap) if initial_gap: initial_gap_num, units = disk.units(initial_gap, default=None) else: initial_gap_num, units = 0, None device_normalized = { "label": label, "pmbr_boot": label == "gpt" and not is_uefi, "partitions": [], } partitions_normalized[device] = device_normalized # Control the start of the next partition start_size = initial_gap_num # Flag to detect if `rest` size was used before rest = False for index, partition in enumerate(device_info.get("partitions", [])): # Detect if there is another partition after we create one # that complete the free space if rest: raise SaltInvocationError( "Partition defined after one filled all the rest free " "space. Use `rest` only on the last partition." ) # Validate the partition type part_type = partition.get("type") if part_type not in VALID_PART_TYPE: raise SaltInvocationError( "Partition type {} not recognized".format(part_type) ) # If part_id is not given, we can create a partition name # based on the position of the partition and the name of # the device # # TODO(aplanas) The partition number will be deduced, so # the require section in mkfs_partition will fail part_id = "{}{}{}".format( device, "p" if __salt__["filters.is_raid"](device) else "", partitions.get("number", index + 1), ) part_id = partition.get("id", part_id) # For parted we usually need to set a ext2 filesystem # type, except for SWAP or UEFI fs_type = {"swap": "linux-swap", "efi": "fat16"}.get(part_type, "ext2") # Check if we are changing units inside the device if partition["size"] == "rest": rest = True # If units is not set, we default to '%' units = units or "%" start = "{}{}".format(start_size, units) end = "100%" else: size, size_units = disk.units(partition["size"]) if units and size_units and units != size_units: raise SaltInvocationError( "Units needs to be the same for the partitions inside " "a device. Found {} but expected {}. Note that " "`initial_gap` is also considered.".format(size_units, units) ) # If units and size_units is not set, we default to UNITS units = units or size_units or UNITS start = "{}{}".format(start_size, units) end = "{}{}".format(start_size + size, units) start_size += size flags = None if part_type in ("raid", "lvm"): flags = [part_type] elif part_type == "boot" and label == "gpt" and not is_uefi: flags = ["bios_grub"] elif part_type == "efi" and label == "gpt" and is_uefi: flags = ["esp"] device_normalized["partitions"].append( { "part_id": part_id, # TODO(aplanas) If msdos we need to create extended # and logical "part_type": "primary", "fs_type": fs_type, "start": start, "end": end, "flags": flags, } ) return partitions_normalized 07070100000027000081A40000000000000000000000016130D1CF00001C66000000000000000000000000000000000000003F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_modules/suseconnect.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import json import logging import re import salt.utils.path from salt.exceptions import CommandExecutionError LOG = logging.getLogger(__name__) __virtualname__ = "suseconnect" def __virtual__(): """ Only load the module if SUSEConnect is installed """ if not salt.utils.path.which("SUSEConnect"): return (False, "SUSEConnect is not installed.") return __virtualname__ # Define not exported variables from Salt, so this can be imported as # a normal module try: __salt__ except NameError: __salt__ = {} def _cmd(cmd): """Utility function to run commands.""" result = __salt__["cmd.run_all"](cmd) if result["retcode"]: raise CommandExecutionError(result["stdout"] + result["stderr"]) return result["stdout"] def register(regcode=None, product=None, email=None, url=None, root=None): """ .. versionadded:: TBD Register SUSE Linux Enterprise installation with the SUSE Customer Center regcode Subscription registration code for the product to be registered. Relates that product to the specified subscription, and enables software repositories for that product. product Specify a product for activation/deactivation. Only one product can be processed at a time. Defaults to the base SUSE Linux Enterprose product on this system. Format: <name>/<version>/<architecture> email Email address for product registration url URL for the registration server (will be saved for the next use) (e.g. https://scc.suse.com) root Path to the root folder, uses the same parameter for zypper CLI Example: .. code-block:: bash salt '*' suseconnect.register regcode='xxxx-yyy-zzzz' salt '*' suseconnect.register product='sle-ha/15.2/x86_64' """ cmd = ["SUSEConnect"] parameters = [ ("regcode", regcode), ("product", product), ("email", email), ("url", url), ("root", root), ] for parameter, value in parameters: if value: cmd.extend(["--{}".format(parameter), str(value)]) return _cmd(cmd) def deregister(product=None, url=None, root=None): """ .. versionadded:: TBD De-register the system and base product, or in cojuntion with 'product', a single extension, and removes all its services installed by SUSEConnect. After de-registration the system no longer consumes a subscription slot in SCC. product Specify a product for activation/deactivation. Only one product can be processed at a time. Defaults to the base SUSE Linux Enterprose product on this system. Format: <name>/<version>/<architecture> url URL for the registration server (will be saved for the next use) (e.g. https://scc.suse.com) root Path to the root folder, uses the same parameter for zypper CLI Example: .. code-block:: bash salt '*' suseconnect.deregister salt '*' suseconnect.deregister product='sle-ha/15.2/x86_64' """ cmd = ["SUSEConnect", "--de-register"] parameters = [("product", product), ("url", url), ("root", root)] for parameter, value in parameters: if value: cmd.extend(["--{}".format(parameter), str(value)]) return _cmd(cmd) def status(root=None): """ .. versionadded:: TBD Get current system registation status. root Path to the root folder, uses the same parameter for zypper CLI Example: .. code-block:: bash salt '*' suseconnect.status """ cmd = ["SUSEConnect", "--status"] parameters = [("root", root)] for parameter, value in parameters: if value: cmd.extend(["--{}".format(parameter), str(value)]) return json.loads(_cmd(cmd)) def _parse_list_extensions(output): """Parse the output of list-extensions result""" # We can extract the indentation using this regex: # r'( {4,}).*\s([-\w]+/[-\w\.]+/[-\w]+).*' return re.findall(r"\s([-\w]+/[-\w\.]+/[-\w]+)", output) def list_extensions(url=None, root=None): """ .. versionadded:: TBD List all extensions and modules avaiable for installation on this system. url URL for the registration server (will be saved for the next use) (e.g. https://scc.suse.com) root Path to the root folder, uses the same parameter for zypper CLI Example: .. code-block:: bash salt '*' suseconnect.list-extensions salt '*' suseconnect.list-extensions url=https://scc.suse.com """ cmd = ["SUSEConnect", "--list-extensions"] parameters = [("url", url), ("root", root)] for parameter, value in parameters: if value: cmd.extend(["--{}".format(parameter), str(value)]) # TODO(aplanas) Implement a better parser return _parse_list_extensions(_cmd(cmd)) def cleanup(root=None): """ .. versionadded:: TBD Remove olf system credential and all zypper services installed by SUSEConnect root Path to the root folder, uses the same parameter for zypper CLI Example: .. code-block:: bash salt '*' suseconnect.cleanup """ cmd = ["SUSEConnect", "--cleanup"] parameters = [("root", root)] for parameter, value in parameters: if value: cmd.extend(["--{}".format(parameter), str(value)]) return _cmd(cmd) def rollback(url=None, root=None): """ .. versionadded:: TBD Revert the registration state in case of a failed migration. url URL for the registration server (will be saved for the next use) (e.g. https://scc.suse.com) root Path to the root folder, uses the same parameter for zypper CLI Example: .. code-block:: bash salt '*' suseconnect.rollback salt '*' suseconnect.rollback url=https://scc.suse.com """ cmd = ["SUSEConnect", "--rollback"] parameters = [("url", url), ("root", root)] for parameter, value in parameters: if value: cmd.extend(["--{}".format(parameter), str(value)]) return _cmd(cmd) 07070100000028000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000002F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_states07070100000029000081A40000000000000000000000016130D1CF00000E8D000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_states/formatted.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging import os.path LOG = logging.getLogger(__name__) __virtualname__ = "formatted" # Define not exported variables from Salt, so this can be imported as # a normal module try: __opts__ __salt__ __states__ except NameError: __opts__ = {} __salt__ = {} __states__ = {} def __virtual__(): """ Formatted can be considered as an extension to blockdev """ return "blockdev.formatted" in __states__ def formatted(name, fs_type="ext4", force=False, **kwargs): """ Manage filesystems of partitions. name The name of the block device fs_type The filesystem it should be formatted as force Force mke2fs to create a filesystem, even if the specified device is not a partition on a block special device. This option is only enabled for ext and xfs filesystems This option is dangerous, use it with caution. """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } fs_type = "swap" if fs_type == "linux-swap" else fs_type if fs_type != "swap": ret = __states__["blockdev.formatted"](name, fs_type, force, **kwargs) return ret if not os.path.exists(name): ret["comment"].append("{} does not exist".format(name)) return ret current_fs = _checkblk(name) if current_fs == "swap": ret["result"] = True return ret elif __opts__["test"]: ret["comment"].append("Changes to {} will be applied ".format(name)) ret["result"] = None return ret cmd = ["mkswap"] if force: cmd.append("-f") if kwargs.pop("check", False): cmd.append("-c") for parameter, argument in ( ("-p", "pagesize"), ("-L", "label"), ("-v", "swapversion"), ("-U", "uuid"), ): if argument in kwargs: cmd.extend([parameter, kwargs.pop(argument)]) cmd.append(name) __salt__["cmd.run"](cmd) current_fs = _checkblk(name) if current_fs == "swap": ret["comment"].append( ("{} has been formatted with {}").format(name, fs_type) ) ret["changes"] = {"new": fs_type, "old": current_fs} ret["result"] = True else: ret["comment"].append("Failed to format {}".format(name)) ret["result"] = False return ret def _checkblk(name): """ Check if the blk exists and return its fstype if ok """ blk = __salt__["cmd.run"]( "blkid -o value -s TYPE {0}".format(name), ignore_retcode=True ) return "" if not blk else blk 0707010000002A000081A40000000000000000000000016130D1CF00001A72000000000000000000000000000000000000003900000000yomi-0.0.1+git.1630589391.4557cfd/salt/_states/images.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging import os import os.path import tempfile import urllib.parse LOG = logging.getLogger(__name__) __virtualname__ = "images" # Define not exported variables from Salt, so this can be imported as # a normal module try: __opts__ __salt__ __utils__ except NameError: __opts__ = {} __salt__ = {} __utils__ = {} # Copied from `images` execution module, as we cannot easly import it VALID_SCHEME = ( "dict", "file", "ftp", "ftps", "gopher", "http", "https", "imap", "imaps", "ldap", "ldaps", "pop3", "pop3s", "rtmp", "rtsp", "scp", "sftp", "smb", "smbs", "smtp", "smtps", "telnet", "tftp", ) VALID_COMPRESSIONS = ("gz", "bz2", "xz") VALID_CHECKSUMS = ("md5", "sha1", "sha224", "sha256", "sha384", "sha512") def __virtual__(): """Images depends on images.dump module""" return "images.dump" in __salt__ def _mount(device): """Mount the device in a temporary place""" dest = tempfile.mkdtemp() res = __salt__["mount.mount"](name=dest, device=device) if res is not True: return None return dest def _umount(path): """Umount and clean the temporary place""" __salt__["mount.umount"](path) __utils__["files.rm_rf"](path) def _checksum_path(root): """Return the path where we will store the last checksum""" return os.path.join(root, __opts__["cachedir"][1:], "images") def _read_current_checksum(device, checksum_type): """Return the checksum of the current image, if any""" checksum = None mnt = _mount(device) if not mnt: return None checksum_file = os.path.join( _checksum_path(mnt), "checksum.{}".format(checksum_type) ) try: checksum = open(checksum_file).read() LOG.info("Checksum file %s content: %s", checksum_file, checksum) except Exception: # If the file cannot be read, we expect that the image needs # to be re-applied eventually LOG.info("Checksum file %s not found", checksum_file) _umount(mnt) return checksum def _save_current_checksum(device, checksum_type, checksum): """Save the checksum of the current image""" result = False mnt = _mount(device) if not mnt: return result checksum_path = _checksum_path(mnt) os.makedirs(checksum_path, exist_ok=True) checksum_file = os.path.join(checksum_path, "checksum.{}".format(checksum_type)) try: checksum_file = open(checksum_file, "w") checksum_file.write(checksum) checksum_file.close() result = True LOG.info("Created checksum file %s content: %s", checksum_file, checksum) except Exception: LOG.error("Error writing checksum file %s", checksum_file) _umount(mnt) return result def _is_dump_needed(device, checksum_type, checksum): return True def dumped(name, device, checksum_type=None, checksum=None, **kwargs): """ Copy an image in the device. name URL of the image. The protocol scheme needs to be available in curl. For example: http, https, scp, sftp, tftp or ftp. The image can be compressed, and the supported extensions are: gz, bz2 and xz device The device or partition where the image will be copied. checksum_type The type of checksum used to validate the image, possible values are 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'. checksum The checksum value. If omitted but a `checksum_type` was set, it will try to download the checksum file from the same URL, replacing the extension with the `checksum_type` Other paramaters send via kwargs will be used during the call for curl. """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } scheme, _, path, *_ = urllib.parse.urlparse(name) if scheme not in VALID_SCHEME: ret["comment"].append("Protocol not valid for URL") return ret # We cannot validate the compression extension, as we can have # non-restricted file names, like '/my-image.ext3' or # 'other-image.raw'. if checksum_type and checksum_type not in VALID_CHECKSUMS: ret["comment"].append("Checksum type not valid") return ret if not checksum_type and checksum: ret["comment"].append("Checksum type not provided") return ret if checksum_type and not checksum: checksum = __salt__["images.fetch_checksum"](name, checksum_type, **kwargs) if not checksum: ret["comment"].append("Checksum no found") return ret if checksum_type: current_checksum = _read_current_checksum(device, checksum_type) if __opts__["test"]: ret["result"] = None if checksum_type: ret["changes"]["image"] = current_checksum != checksum ret["changes"]["checksum cache"] = ret["changes"]["image"] return ret if checksum_type and current_checksum != checksum: result = __salt__["images.dump"]( name, device, checksum_type, checksum, **kwargs ) if result != checksum: ret["comment"].append("Failed writing the image") return ret else: ret["changes"]["image"] = True saved = _save_current_checksum(device, checksum_type, checksum) if not saved: ret["comment"].append("Checksum failed to be saved in the cache") return ret else: ret["changes"]["checksum cache"] = True ret["result"] = True return ret 0707010000002B000081A40000000000000000000000016130D1CF00005AB7000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_states/partitioned.py# # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ import logging import re import disk from salt.exceptions import CommandExecutionError log = logging.getLogger(__name__) __virtualname__ = "partitioned" # Define not exported variables from Salt, so this can be imported as # a normal module try: __grains__ __opts__ __salt__ except NameError: __grains__ = {} __opts__ = {} __salt__ = {} class EnumerateException(Exception): pass def __virtual__(): """ Partitioned depends on partition.mkpart module """ return "partition.mkpart" in __salt__ def _check_label(device, label): """ Check if the label match with the device """ label = {"dos": "msdos"}.get(label, label) res = __salt__["cmd.run"](["parted", "--list", "--machine", "--script"]) line = "".join((line for line in res.splitlines() if line.startswith(device))) return ":{}:".format(label) in line def labeled(name, label): """ Make sure that the label of the partition is properly set. name Device name (/dev/sda, /dev/disk/by-id/scsi-...) label Label of the partition (usually 'gpt' or 'msdos') """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if not label: ret["comment"].append("Label parameter is not optional") return ret if _check_label(name, label): ret["result"] = True ret["comment"].append("Label already set to {}".format(label)) return ret if __opts__["test"]: ret["result"] = None ret["comment"].append("Label will be set to {} in {}".format(label, name)) ret["changes"]["label"] = "Will be set to {}".format(label) return ret __salt__["partition.mklabel"](name, label) if _check_label(name, label): ret["result"] = True msg = "Label set to {} in {}".format(label, name) ret["comment"].append(msg) ret["changes"]["label"] = msg else: ret["comment"].append("Failed to set label to {}".format(label)) return ret def _get_partition_type(device): """ Get partition type of each partition Return dictionary: {number: type, ...} """ cmd = "parted -s {0} print".format(device) out = __salt__["cmd.run_stdout"](cmd) types = re.findall(r"\s*(\d+).*(primary|extended|logical).*", out) return dict(types) def _get_cached_info(device): """ Get the information of a device as a dictionary """ if not hasattr(_get_cached_info, "info"): _get_cached_info.info = {} info = _get_cached_info.info if device not in info: info[device] = __salt__["partition.list"](device)["info"] return info[device] def _invalidate_cached_info(): """ Invalidate the cached information about devices """ if hasattr(_get_cached_info, "info"): delattr(_get_cached_info, "info") def _get_cached_partitions(device, unit="s"): """ Get the partitions as a dictionary """ # `partitions` will be used as a local cache, to avoid multiple # request of the same partition with the same units. Is a # dictionary where the key is the `unit`, as we will make request # of all partitions under this unit. This potentially can low the # complexity algorithm to amortized O(1). if not hasattr(_get_cached_partitions, "partitions"): _get_cached_partitions.partitions = {} # There is a bug in `partition.list`, where `type` is storing # the file system information, to workaround this we get the # partition type using parted and attach it here. _get_cached_partitions.types = _get_partition_type(device) if device not in _get_cached_partitions.partitions: _get_cached_partitions.partitions[device] = {} partitions = _get_cached_partitions.partitions[device] if unit not in partitions: partitions[unit] = __salt__["partition.list"](device, unit=unit) # If the partition comes from a gpt disk, we assign the type # as 'primary' types = _get_cached_partitions.types for number, partition in partitions[unit]["partitions"].items(): partition["type"] = types.get(number, "primary") return partitions[unit]["partitions"] def _invalidate_cached_partitions(): """ Invalidate the cached information about partitions """ if hasattr(_get_cached_partitions, "partitions"): delattr(_get_cached_partitions, "partitions") delattr(_get_cached_partitions, "types") OVERLAPPING_ERROR = 0.75 def _check_partition(device, number, part_type, start, end): """ Check if the proposed partition match the current one. Returns a tri-state value: - `True`: the proposed partition match - `False`: the proposed partition do not match - `None`: the proposed partition is a new partition """ # The `start` and `end` fields are expressed with units (the same # kind of units that `parted` allows). To make a fair comparison # we need to normalize each field to the same units that we can # use to read the current partitions. A good candidate is sector # ('s'). The problem is that we need to reimplement the same # conversion logic from `parted` here [1], as we need the same # round logic when we convert from 'MiB' to 's', for example. # # To avoid this duplicity of code we can do a trick: for each # field in the proposed partition we request a `partition.list` # with the same unit. We make `parted` to make the conversion for # us, in exchange for an slower algorithm. # # We can change it once we decide to take care of alignment. # # [1] Check libparted/unit.c number = str(number) partitions = _get_cached_partitions(device) if number not in partitions: return None if part_type != partitions[number]["type"]: return False for value, name in ((start, "start"), (end, "end")): value, unit = disk.units(value) p_value = _get_cached_partitions(device, unit)[number][name] p_value = disk.units(p_value)[0] min_value = value - OVERLAPPING_ERROR max_value = value + OVERLAPPING_ERROR if not min_value <= p_value <= max_value: return False return True def _get_first_overlapping_partition(device, start): """ Return the first partition that contains the start point. """ # Check if there is a partition in the system that start at # specified point. value, unit = disk.units(start) value += OVERLAPPING_ERROR partitions = _get_cached_partitions(device, unit) partition_number = None partition_start = 0 for number, partition in partitions.items(): p_start = disk.units(partition["start"])[0] p_end = disk.units(partition["end"])[0] if p_start <= value <= p_end: if partition_number is None or partition_start < p_start: partition_number = number partition_start = p_start return partition_number def _get_partition_number(device, part_type, start, end): """ Return a partition number for a [start, end] range and a partition type. If the range is allocated and the partition type match, return the partition number. If the type do not match but is a logical partition inside an extended one, return the next partition number. If the range is not allocated, return the next partition number. """ unit = disk.units(start)[1] partitions = _get_cached_partitions(device, unit) # Check if there is a partition in the system that start or # containst the start point number = _get_first_overlapping_partition(device, start) if number: if partitions[number]["type"] == part_type: return number elif not (partitions[number]["type"] == "extended" and part_type == "logical"): raise EnumerateException("Do not overlap partitions") def __primary_partition_free_slot(partitions, label): if label == "msdos": max_primary = 4 else: max_primary = 1024 for i in range(1, max_primary + 1): i = str(i) if i not in partitions: return i # The partition is not already there, we guess the next number label = _get_cached_info(device)["partition table"] if part_type == "primary": candidate = __primary_partition_free_slot(partitions, label) if not candidate: raise EnumerateException("No free slot for primary partition") return candidate elif part_type == "extended": if label == "gpt": raise EnumerateException("Extended partitions not allowed in gpt") if "extended" in (info["type"] for info in partitions.values()): raise EnumerateException("Already found a extended partition") candidate = __primary_partition_free_slot(partitions, label) if not candidate: raise EnumerateException("No free slot for extended partition") return candidate elif part_type == "logical": if label == "gpt": raise EnumerateException("Extended partitions not allowed in gpt") if "extended" not in (part["type"] for part in partitions.values()): raise EnumerateException("Missing extended partition") candidate = max( ( int(part["number"]) for part in partitions.values() if part["type"] == "logical" ), default=4, ) return str(candidate + 1) def _get_partition_flags(device, number): """ Return the current list of flags for a partition. """ def _is_valid(flag): """Return True if is a valid flag""" if flag == "swap" or flag.startswith("type="): return False return True result = [] number = str(number) partitions = __salt__["partition.list"](device)["partitions"] if number in partitions: # In parted the field for flags is reused to mark other # situations, so we need to remove values that do not # represent flags flags = partitions[number]["flags"].split(", ") result = [flag for flag in flags if flag and _is_valid(flag)] return result def mkparted(name, part_type, fs_type=None, start=None, end=None, flags=None): """ Make sure that a partition is allocated in the disk. name Device or partition name. If the name is like /dev/sda, parted will take care of creating the partition on the next slot. If the name is like /dev/sda1, we will consider partition 1 as a reference for the match. part_type Type of partition, should be one of "primary", "logical", or "extended". fs_type Expected filesystem, following the parted names. start Start of the partition (in parted units) end End of the partition (in parted units) flags List of flags present in the partition """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if part_type not in ("primary", "extended", "logical"): ret["comment"].append("Partition type not recognized") if not start or not end: ret["comment"].append("Parameters start and end are not optional") # Normalize fs_type. Some versions of salt contains a bug were # only a subset of file systems are valid for mkpart, even if are # supported by parted. As mkpart do not format the partition, is # safe to make a normalization here. Eventually this is only used # to set the type in the flag section (partition id). # # We can drop this check in the next version of salt. if fs_type and fs_type not in set( [ "ext2", "fat32", "fat16", "linux-swap", "reiserfs", "hfs", "hfs+", "hfsx", "NTFS", "ufs", "xfs", "zfs", ] ): fs_type = "ext2" flags = flags if flags else [] # If the user do not provide any partition number we get generate # the next available for the partition type device_md, device_no_md, number = re.search( r"(?:(/dev/md[^p]+)p?|(\D+))(\d*)", name ).groups() device = device_md if device_md else device_no_md if not number: try: number = _get_partition_number(device, part_type, start, end) except EnumerateException as e: ret["comment"].append(str(e)) # If at this point we have some comments, we return with a fail if ret["comment"]: return ret # Check if the partition is already there or we need to create a # new one partition_match = _check_partition(device, number, part_type, start, end) if partition_match: ret["result"] = True ret["comment"].append("Partition {}{} already in place".format(device, number)) return ret elif partition_match is None: ret["changes"]["new"] = "Partition {}{} will be created".format(device, number) elif partition_match is False: ret["comment"].append( "Partition {}{} cannot be replaced".format(device, number) ) return ret if __opts__["test"]: ret["result"] = None return ret if partition_match is None: # TODO(aplanas) with parted we cannot force a partition number res = __salt__["partition.mkpart"](device, part_type, fs_type, start, end) ret["changes"]["output"] = res # Wipe the filesystem information from the partition to remove # old data that was on the disk. As a side effect, this will # force the mkfs state to happend. __salt__["disk.wipe"]("{}{}".format(device, number)) _invalidate_cached_info() _invalidate_cached_partitions() # The first time that we create a partition we do not have a # partition number for it if not number: number = _get_partition_number(device, part_type, start, end) partition_match = _check_partition(device, number, part_type, start, end) if partition_match: ret["result"] = True elif not partition_match: ret["comment"].append( "Partition {}{} fail to be created".format(device, number) ) ret["result"] = False # We set the correct flags for the partition current_flags = _get_partition_flags(device, number) flags_to_set = set(flags) - set(current_flags) flags_to_unset = set(current_flags) - set(flags) for flag in flags_to_set: try: out = __salt__["partition.set"](device, number, flag, "on") except CommandExecutionError as e: out = e if out: ret["comment"].append( "Error setting flag {} in {}{}: {}".format(flag, device, number, out) ) ret["result"] = False else: ret["changes"][flag] = True for flag in flags_to_unset: try: out = __salt__["partition.set"](device, number, flag, "off") except CommandExecutionError as e: out = e if out: ret["comment"].append( "Error unsetting flag {} in {}{}: {}".format(flag, device, number, out) ) ret["result"] = False else: ret["changes"][flag] = False return ret def _check_partition_name(device, number, name): """ Check if the partition have this name. Returns a tri-state value: - `True`: the partition already have this label - `False`: the partition do not have this label - `None`: there is not such partition """ number = str(number) partitions = _get_cached_partitions(device) if number in partitions: return partitions[number]["name"] == name def named(name, device, partition=None): """ Make sure that a gpt partition have set a name. name Name or label for the partition device Device name (/dev/sda, /dev/disk/by-id/scsi-...) or partition partition Partition number (can be in the device) """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if not partition: device, partition = re.search(r"(\D+)(\d*)", device).groups() if not partition: ret["comment"].append("Partition number not provided") if not _check_label(device, "gpt"): ret["comment"].append("Only gpt partitions can be named") name_match = _check_partition_name(device, partition, name) if name_match: ret["result"] = True ret["comment"].append( "Name of the partition {}{} is " 'already "{}"'.format(device, partition, name) ) elif name_match is None: ret["comment"].append("Partition {}{} not found".format(device, partition)) if ret["comment"]: return ret if __opts__["test"]: ret["comment"].append( "Partition {}{} will be named " '"{}"'.format(device, partition, name) ) ret["changes"]["name"] = "Name will be set to {}".format(name) return ret changes = __salt__["partition.name"](device, partition, name) _invalidate_cached_info() _invalidate_cached_partitions() if _check_partition_name(device, partition, name): ret["result"] = True ret["comment"].append("Name set to {} in {}{}".format(name, device, partition)) ret["changes"]["name"] = changes else: ret["comment"].append("Failed to set name to {}".format(name)) return ret def _check_disk_flags(device, flag): """ Return True if the flag for a device is already set. """ flags = __salt__["partition.list"](device)["info"]["disk flags"] return flag in flags def disk_set(name, flag, enabled=True): """ Make sure that a disk flag is set or unset. name Device name (/dev/sda, /dev/disk/by-id/scsi-...) flag A valid parted disk flag (see ``parted.disk_set``) enabled Boolean value CLI Example: .. code-block:: bash salt '*' partitioned.disk_set /dev/sda pmbr_boot salt '*' partitioned.disk_set /dev/sda pmbr_boot False """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } is_flag = _check_disk_flags(name, flag) if enabled == is_flag: ret["result"] = True ret["comment"].append( "Flag {} in {} already {}".format(flag, name, "set" if enabled else "unset") ) return ret if __opts__["test"]: ret["comment"].append( "Flag {} in {} will be {}".format(flag, name, "set" if enabled else "unset") ) ret["changes"][flag] = enabled return ret __salt__["partition.disk_set"](name, flag, "on" if enabled else "off") is_flag = _check_disk_flags(name, flag) if enabled == is_flag: ret["result"] = True ret["comment"].append( "Flag {} {} in {}".format(flag, "set" if enabled else "unset", name) ) ret["changes"][flag] = enabled else: ret["comment"].append( "Failed to {} {} in {}".format("set" if enabled else "unset", flag, name) ) return ret def _check_partition_flags(device, number, flag): """ Return True if the flag for a partition is already set. Returns a tri-state value: - `True`: the partition already have this flag - `False`: the partition do not have this flag - `None`: there is not such partition """ number = str(number) partitions = __salt__["partition.list"](device)["partitions"] if number in partitions: return flag in partitions[number]["flags"] def partition_set(name, flag, partition=None, enabled=True): """ Make sure that a partition flag is set or unset. name Device name (/dev/sda, /dev/disk/by-id/scsi-...) or partition flag A valid parted disk flag (see ``parted.disk_set``) partition Partition number (can be in the device name) enabled Boolean value CLI Example: .. code-block:: bash salt '*' partitioned.partition_set /dev/sda1 bios_grub salt '*' partitioned.partition_set /dev/sda bios_grub 1 False """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if not partition: name, partition = re.search(r"(\D+)(\d*)", name).groups() if not partition: ret["comment"].append("Partition number not provided") is_flag = _check_partition_flags(name, partition, flag) if enabled == is_flag: ret["result"] = True ret["comment"].append( "Flag {} in {}{} already {}".format( flag, name, partition, "set" if enabled else "unset" ) ) elif is_flag is None: ret["comment"].append("Partition {}{} not found".format(name, partition)) if ret["comment"]: return ret if __opts__["test"]: ret["comment"].append( "Flag {} in {}{} will be {}".format( flag, name, partition, "set" if enabled else "unset" ) ) ret["changes"][flag] = enabled return ret __salt__["partition.set"](name, partition, flag, "on" if enabled else "off") is_flag = _check_partition_flags(name, partition, flag) if enabled == is_flag: ret["result"] = True ret["comment"].append( "Flag {} {} in {}{}".format( flag, "set" if enabled else "unset", name, partition ) ) ret["changes"][flag] = enabled else: ret["comment"].append( "Failed to {} {} in {}{}".format( "set" if enabled else "unset", flag, name, partition ) ) return ret 0707010000002C000081A40000000000000000000000016130D1CF0000284A000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/salt/_states/snapper_install.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import functools import logging import os.path import tempfile import traceback log = logging.getLogger(__name__) INSTALLATION_HELPER = "/usr/lib/snapper/installation-helper" SNAPPER = "/usr/bin/snapper" __virtualname__ = "snapper_install" # Define not exported variables from Salt, so this can be imported as # a normal module try: __grains__ __opts__ __salt__ __utils__ except NameError: __grains__ = {} __opts__ = {} __salt__ = {} __utils__ = {} def __virtual__(): """ snapper_install requires the installation helper binary. """ if not os.path.exists(INSTALLATION_HELPER): return (False, "{} binary not found".format(INSTALLATION_HELPER)) return True def _mount(device): """ Mount the device in a temporary place. """ dest = tempfile.mkdtemp() res = __salt__["mount.mount"](name=dest, device=device) if res is not True: log.error("Cannot mount device %s in %s", device, dest) _umount(dest) return None return dest def _umount(path): """ Umount and clean the temporary place. """ __salt__["mount.umount"](path) __utils__["files.rm_rf"](path) def __mount_device(action): """ Small decorator to makes sure that the mount and umount happends in a transactional way. """ @functools.wraps(action) def wrapper(*args, **kwargs): device = kwargs.get("device", args[1] if len(args) > 1 else None) ret = { "name": device, "result": False, "changes": {}, "comment": ["Some error happends during the operation."], } try: dest = _mount(device) if not dest: msg = "Device {} cannot be mounted".format(device) ret["comment"].append(msg) kwargs["__dest"] = dest ret = action(*args, **kwargs) except Exception as e: log.error("""Traceback: {}""".format(traceback.format_exc())) ret["comment"].append(e) finally: _umount(dest) return ret return wrapper def step_one(name, device, description): """ Step one of the installation-helper tool name Name of the state device Device where to install snapper description Description for the fist snapshot """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } # Mount the device and check if /etc/snapper/configs is present dest = _mount(device) if not dest: ret["comment"].append( "Fail mounting {} in temporal directory {}".format(device, dest) ) return ret is_configs = os.path.exists(os.path.join(dest, "etc/snapper/configs")) _umount(dest) if is_configs: ret["result"] = None if __opts__["test"] else True ret["comment"].append("Step one already applied to {}".format(device)) return ret if __opts__["test"]: ret["comment"].append("Step one will be applied to {}".format(device)) return ret cmd = [ INSTALLATION_HELPER, "--step", "1", "--device", device, "--description", description, ] res = __salt__["cmd.run_all"](cmd) if res["retcode"] or res["stderr"]: ret["comment"].append("Failed to execute step one {}".format(res["stderr"])) else: ret["result"] = True ret["changes"]["step one"] = True return ret @__mount_device def step_two(name, device, prefix=None, __dest=None): """ Step two of the installation-helper tool name Name of the state device Device where to install snapper prefix Default root prefix for the subvolumes """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } snapshots = os.path.join(__dest, ".snapshots") if os.path.exists(snapshots): ret["result"] = None if __opts__["test"] else True ret["comment"].append("Step two aleady applied to {}".format(device)) return ret if __opts__["test"]: ret["comment"].append("Step two will be applied to {}".format(device)) return ret cmd = [ INSTALLATION_HELPER, "--step", "2", "--device", device, "--root-prefix", __dest, ] if prefix: cmd.extend(["--default-subvolume-name", prefix]) res = __salt__["cmd.run_all"](cmd) if res["retcode"] or res["stderr"]: ret["comment"].append("Failed to execute step two {}".format(res["stderr"])) else: ret["result"] = True ret["changes"]["step two"] = True # Internally step two mounts a new subvolume called .snapshots for i in range(5): res = __salt__["mount.umount"](snapshots) if res is not True: log.warning("Retry %s: Failed to umount %s: %s", i, snapshots, res) else: break else: # We fail to umount .snapshots directory, bit the installation # step was properly executed, so we still return True ret["comment"].append("Failed to umount {}: {}".format(snapshots, res)) return ret def step_four(name, root): """ Step four of the installation-helper tool name Name of the state root Target directory where to chroot """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if os.path.exists(os.path.join(root, ".snapshots/grub-snapshot.cfg")): ret["result"] = None if __opts__["test"] else True ret["comment"].append("Step four already applied to {}".format(root)) return ret if __opts__["test"]: ret["comment"].append("Step four will be applied to {}".format(root)) return ret cmd = [INSTALLATION_HELPER, "--step", "4"] res = __salt__["cmd.run_chroot"](root, cmd) if res["retcode"] or res["stderr"]: ret["comment"].append("Failed to execute step four {}".format(res["stderr"])) return ret # Set the initial configuration and quota as YaST is doing cmd = [ SNAPPER, "--no-dbus", "set-config", "NUMBER_CLEANUP=yes", "NUMBER_LIMIT=2-10", "NUMBER_LIMIT_IMPORTANT=4-10", "TIMELINE_CREATE=no", ] res = __salt__["cmd.run_chroot"](root, cmd) if res["retcode"] or res["stderr"]: ret["comment"].append( "Failed to set configuration in step four {}".format(res["stderr"]) ) return ret cmd = [SNAPPER, "--no-dbus", "setup-quota"] res = __salt__["cmd.run_chroot"](root, cmd) if res["retcode"] or res["stderr"]: ret["comment"].append( "Failed to set quota in step four {}".format(res["stderr"]) ) return ret ret["result"] = True ret["changes"]["step four"] = True return ret def step_five(name, root, snapshot_type, description, important, cleanup): """ Step five of the installation-helper tool name Name of the state root Target directory where to chroot snapshot_type Type of snapshot: {single, pre, post} description Description for the snapshot important Is the snapshot important cleanup Type or snapper cleanup angorithm: {number, timeline, empty-pre-post} """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if snapshot_type not in ("single", "pre", "post"): ret["comment"].append("Value for snapshot_type not recognized") return ret if not description: ret["comment"].append("Value for description is empty") return ret if cleanup not in ("number", "timeline", " empty-pre-post "): ret["comment"].append("Value for cleanup not recognized") return ret cmd = [SNAPPER, "--no-dbus", "list"] res = __salt__["cmd.run_chroot"](root, cmd) if res["retcode"] or res["stderr"]: ret["comment"].append( "Failed to list snapshots in step five {}".format(res["stderr"]) ) return ret if description in res["stdout"]: ret["result"] = None if __opts__["test"] else True ret["comment"].append("Step five already applied to {}".format(root)) return ret if __opts__["test"]: ret["comment"].append("Step five will be applied to {}".format(root)) return ret cmd = [ INSTALLATION_HELPER, "--step", "5", "--snapshot-type", snapshot_type, "--description", '"{}"'.format(description), "--userdata", "important={}".format("yes" if important else "no"), "--cleanup", cleanup, ] res = __salt__["cmd.run_chroot"](root, cmd) if res["retcode"] or res["stderr"]: ret["comment"].append("Failed to execute step five {}".format(res["stderr"])) else: ret["result"] = True ret["changes"]["step five"] = True return ret 0707010000002D000081A40000000000000000000000016130D1CF00001A3F000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_states/suseconnect.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """ :maintainer: Alberto Planas <aplanas@suse.com> :maturity: new :depends: None :platform: Linux """ from __future__ import absolute_import, print_function, unicode_literals import logging import re from salt.exceptions import CommandExecutionError LOG = logging.getLogger(__name__) __virtualname__ = "suseconnect" # Define not exported variables from Salt, so this can be imported as # a normal module try: __opts__ __salt__ __states__ except NameError: __opts__ = {} __salt__ = {} __states__ = {} def __virtual__(): """ SUSEConnect module is required """ return "suseconnect.register" in __salt__ def _status(root): """ Return the list of resitered modules and subscriptions """ status = __salt__["suseconnect.status"](root=root) registered = [ "{}/{}/{}".format(i["identifier"], i["version"], i["arch"]) for i in status if i["status"] == "Registered" ] subscriptions = [ "{}/{}/{}".format(i["identifier"], i["version"], i["arch"]) for i in status if i.get("subscription_status") == "ACTIVE" ] return registered, subscriptions def _is_registered(product, root): """ Check if a product is registered """ # If the user provides a product, and the product is registered, # or if the user do not provide a product name, but some # subscription is active, we consider that there is nothing else # to do. registered, subscriptions = _status(root) if (product and product in registered) or (not product and subscriptions): return True return False def registered(name, regcode=None, product=None, email=None, url=None, root=None): """ .. versionadded:: TBD Register SUSE Linux Enterprise installation with the SUSE Customer Center name If follows the product name rule, will be the name of the product. regcode Subscription registration code for the product to be registered. Relates that product to the specified subscription, and enables software repositories for that product. product Specify a product for activation/deactivation. Only one product can be processed at a time. Defaults to the base SUSE Linux Enterprose product on this system. Format: <name>/<version>/<architecture> email Email address for product registration url URL for the registration server (will be saved for the next use) (e.g. https://scc.suse.com) root Path to the root folder, uses the same parameter for zypper """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if not product and re.match(r"[-\w]+/[-\w\.]+/[-\w]+", name): product = name name = product if product else "default" if _is_registered(product, root): ret["result"] = True ret["comment"].append("Product or module {} already registered".format(name)) return ret if __opts__["test"]: ret["result"] = None ret["comment"].append("Product or module {} would be registered".format(name)) ret["changes"][name] = True return ret try: __salt__["suseconnect.register"]( regcode, product=product, email=email, url=url, root=root ) except CommandExecutionError as e: ret["comment"].append("Error registering {}: {}".format(name, e)) return ret ret["changes"][name] = True if _is_registered(product, root): ret["result"] = True ret["comment"].append("Product or module {} registered".format(name)) else: ret["comment"].append("Product or module {} failed to register".format(name)) return ret def deregistered(name, product=None, url=None, root=None): """ .. versionadded:: TBD De-register the system and base product, or in cojuntion with 'product', a single extension, and removes all its services installed by SUSEConnect. After de-registration the system no longer consumes a subscription slot in SCC. name If follows the product name rule, will be the name of the product. product Specify a product for activation/deactivation. Only one product can be processed at a time. Defaults to the base SUSE Linux Enterprose product on this system. Format: <name>/<version>/<architecture> url URL for the registration server (will be saved for the next use) (e.g. https://scc.suse.com) root Path to the root folder, uses the same parameter for zypper """ ret = { "name": name, "result": False, "changes": {}, "comment": [], } if not product and re.match(r"[-\w]+/[-\w\.]+/[-\w]+", name): product = name name = product if product else "default" if not _is_registered(product, root): ret["result"] = True ret["comment"].append("Product or module {} already deregistered".format(name)) return ret if __opts__["test"]: ret["result"] = None ret["comment"].append("Product or module {} would be deregistered".format(name)) ret["changes"][name] = True return ret try: __salt__["suseconnect.deregister"](product=product, url=url, root=root) except CommandExecutionError as e: ret["comment"].append("Error deregistering {}: {}".format(name, e)) return ret ret["changes"][name] = True if not _is_registered(product, root): ret["result"] = True ret["comment"].append("Product or module {} deregistered".format(name)) else: ret["comment"].append("Product or module {} failed to deregister".format(name)) return ret 0707010000002E000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000002E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/_utils0707010000002F000081A40000000000000000000000016130D1CF00000689000000000000000000000000000000000000003600000000yomi-0.0.1+git.1630589391.4557cfd/salt/_utils/disk.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import re class ParseException(Exception): pass def units(value, default="MB"): """ Split a value expressed (optionally) with units. Returns the tuple (value, unit) """ valid_units = ( "s", "B", "kB", "MB", "MiB", "GB", "GiB", "TB", "TiB", "%", "cyl", "chs", "compact", ) match = re.search(r"^([\d.]+)(\D*)$", str(value)) if match: value, unit = match.groups() unit = unit if unit else default if unit in valid_units: return (float(value), unit) else: raise ParseException("{} not recognized as a valid unit".format(unit)) raise ParseException("{} cannot be parsed".format(value)) 07070100000030000081A40000000000000000000000016130D1CF00004145000000000000000000000000000000000000003400000000yomi-0.0.1+git.1630589391.4557cfd/salt/_utils/lp.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. EQ = "=" LTE = "<=" GTE = ">=" MINIMIZE = "-" MAXIMIZE = "+" def _vec_scalar(vector, scalar): """Multiply a vector by an scalar.""" return [v * scalar for v in vector] def _vec_vec_scalar(vector_a, vector_b, scalar): """Linear combination of two vectors and a scalar.""" return [a * scalar + b for a, b in zip(vector_a, vector_b)] def _vec_plus_vec(vector_a, vector_b): """Sum of two vectors.""" return [a + b for a, b in zip(vector_a, vector_b)] class Model: """Class that represent a linear programming problem.""" def __init__(self, variables): """Create a model with named variables.""" # All variables are bound and >= 0. We do not support # unbounded variables. self.variables = variables self._constraints = [] self._cost_function = None self._slack_variables = [] self._standard_constraints = [] self._standard_cost_function = None self._canonical_constraints = [] self._canonical_cost_function = None self._canonical_artificial_function = None def add_constraint(self, coefficients, operator, free_term): """Add a constraint in non-standard form.""" # We can express constraints in a general form as: # # a_1 x_1 + a_2 x_2 + ... + a_n x_n <= b # # For this case the values are: # * coefficients = [a_1, a_2, ..., a_n] # * operator = '<=' # * free_term = b # assert len(coefficients) == len(self.variables), ( "Coefficients length must match the number of variables" ) assert operator in (EQ, LTE, GTE), "Operator not valid" self._constraints.append((coefficients, operator, free_term)) def add_cost_function(self, action, coefficients, free_term): """Add a cost function in non-standard form.""" # We can express a cost function as: # # Miminize: z = c_1 x_1 + c_2 x_2 + ... + c_n x_n + z_0 # # For this case the values are: # * action = '-' # * coefficients = [c_1, c_2, ..., c_n] # * free_term = z_0 # assert action in (MINIMIZE, MAXIMIZE), "Action not valid" assert len(coefficients) == len(self.variables), ( "Coefficients length must match the number of variables" ) self._cost_function = (action, coefficients, free_term) def _coeff(self, coefficients): """Translate a coefficients dictionary into a list.""" coeff = [0] * len(self.variables) for idx, variable in enumerate(self.variables): coeff[idx] = coefficients.get(variable, 0) return coeff def add_constraint_named(self, coefficients, operator, free_term): """Add a constraint in non-standard form.""" self.add_constraint(self._coeff(coefficients), operator, free_term) def add_cost_function_named(self, action, coefficients, free_term): """Add a cost function in non-standard form.""" self.add_cost_function(action, self._coeff(coefficients), free_term) def simplex(self): """Resolve a linear programing model.""" self._convert_to_standard_form() self._convert_to_canonical_form() tableau = self._build_tableau_canonical_form() tableau.simplex() tableau.drop_artificial() tableau.simplex() constraints = tableau.constraints() solution = {i: 0 for i in self.variables} for idx_cons, idx_var in enumerate(tableau._basic_variables): try: variable = self.variables[idx_var] solution[variable] = constraints[idx_cons][-1] except IndexError: pass return solution def _convert_to_standard_form(self): """Convert constraints and cost function to standard form.""" slack_vars = len([c for c in self._constraints if c[1] != EQ]) self._standard_constraints = [] slack_var_idx = 0 base_slack_var_idx = len(self.variables) for coefficients, operator, free_term in self._constraints: slack_coeff = [0] * slack_vars if operator in (LTE, GTE): slack_coeff[slack_var_idx] = 1 if operator == LTE else -1 self._slack_variables.append(base_slack_var_idx + slack_var_idx) slack_var_idx += 1 self._standard_constraints.append((coefficients + slack_coeff, free_term)) # Adjust the cost function action, coefficients, free_term = self._cost_function slack_coeff = [0] * slack_vars if action == MAXIMIZE: coefficients = _vec_scalar(coefficients, -1) self._standard_cost_function = (coefficients + slack_coeff, -free_term) def _convert_to_canonical_form(self): """Convert the model into canonical form.""" artificial_vars = len(self._constraints) self._canonical_constraints = [] artificial_var_idx = 0 slack_vars = len([c for c in self._constraints if c[1] != EQ]) coeff_acc = [0] * (len(self.variables) + slack_vars) free_term_acc = 0 for coefficients, free_term in self._standard_constraints: if free_term < 0: coefficients = _vec_scalar(coefficients, -1) free_term *= -1 artificial_coeff = [0] * artificial_vars artificial_coeff[artificial_var_idx] = 1 artificial_var_idx += 1 self._canonical_constraints.append( (coefficients + artificial_coeff, free_term) ) coeff_acc = _vec_plus_vec(coeff_acc, coefficients) free_term_acc += free_term coefficients, free_term = self._standard_cost_function artificial_coeff = [0] * artificial_vars self._canonical_cost_function = (coefficients + artificial_coeff, free_term) coeff_acc = _vec_scalar(coeff_acc, -1) self._canonical_artificial_function = ( coeff_acc + artificial_coeff, -free_term_acc, ) def _build_tableau_canonical_form(self): """Build the tableau related with the canonical form.""" # Total number of variables n = len(self._canonical_artificial_function[0]) # Basic variables (in canonical form there is one per constraint) m = len(self._constraints) tableau = Tableau(n, m) canonical_constraints = enumerate(self._canonical_constraints) for (idx, (coefficients, free_term)) in canonical_constraints: tableau.add_constraint(coefficients + [free_term], n - m + idx) coefficients, free_term = self._canonical_cost_function tableau.add_cost_function(coefficients + [free_term]) coefficients, free_term = self._canonical_artificial_function tableau.add_artificial_function(coefficients + [free_term]) return tableau def _str_coeff(self, coefficients): """Transform a coefficient array into a string.""" result = [] for coefficient, variable in zip(coefficients, self.variables): if result: result.append("+" if coefficient >= 0 else "-") coefficient = abs(coefficient) result.append("{} {}".format(coefficient, variable)) return " ".join(result) def __str__(self): result = [] """String representation of a model.""" if self._cost_function: result.append( {MINIMIZE: "Minimize:", MAXIMIZE: "Maximize:"}[self._cost_function[0]] ) free_term = self._cost_function[2] free_term_sign = "+" if free_term >= 0 else "-" z = " ".join( ( self._str_coeff(self._cost_function[1]), free_term_sign, str(abs(free_term)), ) ) result.append(" " + z) result.append("") result.append("Subject to:") for constraint in self._constraints: c = " ".join( (self._str_coeff(constraint[0]), constraint[1], str(constraint[2])) ) result.append(" " + c) c = ", ".join(self.variables) + " >= 0" result.append(" " + c) return "\n".join(result) class Tableau: # To sumarize the steps of the simplex method, starting with the # problem in canonical form. # # 1. if all c_j >= 0, the minimum value of the objective function # has been achieved. # # 2. If there exists an s such that c_s < 0 and a_{is} <= 0 for # all i, the objective function is not bounded below. # # 3. Otherwise, pivot. To determine the pivot term: # # (a) Pivot in any column with a negative c_j term. If there # are several negative c_j's, pivoting in the column with the # smallest c_j may reduce the total number of steps necessary # to complete the problem. Assume that we pivot column s. # # (b) To determine the row of the pivot of the pivot term, find # that row, say row r, such that # # b_r / a_{rs} = Min { b_i / a_{is}: a_{is} > 0 } # # Notice that here only those ratios b_i / a_{is} with a_{is} > # 0 are considered. If the minimum of there ratios is attained # in several rows, a simple rule such as choosing the row with # the smallest index can be used to determine the pivoting row. # # 4. After pivoting, the problem remains in canonical form at a # different basic feasible solution. Now return to step 1. # # If the problem contains a degenerate b.f.s., proceed as above. def __init__(self, n, m): self.n = n self.m = m self._basic_variables = [] self._tableau = [] self._artificial = False def add_constraint(self, constraint, basic_variable): """Add a contraint into the tableau.""" assert len(constraint) == self.n + 1, "Wrong size for the constraint" assert ( basic_variable not in self._basic_variables ), "Basic variable is already registered" assert ( len(self._basic_variables) == len(self._tableau) and len(self._tableau) < self.m ), "Too many constraints registered" self._basic_variables.append(basic_variable) self._tableau.append(constraint) def add_cost_function(self, cost_function): """Add the const function in the tableau.""" assert len(cost_function) == self.n + 1, "Wrong size for the cost function" assert ( len(self._basic_variables) == len(self._tableau) and len(self._tableau) == self.m ), "Too few constraints registered" self._tableau.append(cost_function) def add_artificial_function(self, artificial_function): """Add the artificial function in the tableau.""" assert ( len(artificial_function) == self.n + 1 ), "Wrong size for the cost function" assert ( len(self._basic_variables) == len(self._tableau) - 1 and len(self._tableau) == self.m + 1 ), ("Too few constraints or not cost function registered") self._artificial = True self._tableau.append(artificial_function) def constraints(self): """Return the constraints in the tableau.""" last = -1 if not self._artificial else -2 return self._tableau[:last] def cost_function(self): """Return the cost function in the tableau.""" # If we use the artificial cost function, is still in the last # position. return self._tableau[-1] def drop_artificial(self): """Transform the tableau in one without artificial variables.""" assert self._artificial, "Tableau already without artificial variables" assert self.is_minimum(), "Tableau is not in minimum state" # Check that the basic variables are not artificial variables artificial_variables = range(self.n - self.m, self.n) assert not any( i in self._basic_variables for i in artificial_variables ), "At least one artificial variable is a basic variable" # Remove the artificial cost function self._tableau.pop() # Drop all artificial variable coefficients tableau = [] for line in self._tableau: tableau.append(line[: -self.m - 1] + [line[-1]]) self._tableau = tableau self._artificial = False def simplex(self): """Resolve the constraints via the simplex algorithm.""" while not self.is_minimum(): column = self._get_pivoting_column() row = self._get_pivoting_row(column) self._pivote(row, column) self._basic_variables[row] = column def is_canonical(self): """Check if is in canonical form.""" result = True # The system of constraints is in canonical form for idx, constraint in zip(self._basic_variables, self.constraints()): result = result and all( constraint[i] == (1 if idx == i else 0) for i in self._basic_variables ) # We need to check that the associated basic solution is # feasible. But we separate this check in a different method. # result = result and self.is_basic_feasible_solution() # The objective function is expressed in therms of only the # nonbasic variables cost_function = self.cost_function() result = result and all(cost_function[i] == 0 for i in self._basic_variables) return result def is_minimum(self): """Check if the cost function is already minimized.""" return all(c >= 0 for c in self.cost_function()[:-1]) def is_basic_feasible_solution(self): """Check if there is a basic feasible solution.""" assert self.is_canonical(), "Tableau is not in canonical form" if self._artificial: assert self.is_minimum(), ( "If there are artificial variables, we need to be minimized." ) return self.cost_functions()[-1] == 0 else: return all(c[-1] >= 0 for c in self.constraints()) def is_bound(self): """Check if the cost function is bounded.""" candidates_idx = [i for i, c in enumerate(self.cost_function()[:-1]) if c < 0] return all( all(row[i] >= 0 for row in self.constraints()) for i in candidates_idx ) def _get_pivoting_column(self): """Returm the column number where we can pivot.""" candidates = [(i, c) for i, c in enumerate(self.cost_function()[:-1]) if c < 0] assert candidates, "Cost function already minimal." return min(candidates, key=lambda x: x[1])[0] def _get_pivoting_row(self, column): """Return the row number where we can pivot.""" candidates = [ (i, row[-1] / row[column]) for i, row in enumerate(self.constraints()) if row[column] > 0 ] # NOTE(aplanas): Not sure that this is the case assert candidates, "Not basic feasible solution found." return min(candidates, key=lambda x: x[1])[0] def _pivote(self, row, column): """Pivote the tableau in (row, column).""" # Normalize the row vec = _vec_scalar(self._tableau[row], 1 / self._tableau[row][column]) self._tableau[row] = vec for row_b, vec_b in enumerate(self._tableau): if row_b != row: self._tableau[row_b] = _vec_vec_scalar(vec, vec_b, -vec_b[column]) 07070100000031000081A40000000000000000000000016130D1CF000006CF000000000000000000000000000000000000003200000000yomi-0.0.1+git.1630589391.4557cfd/salt/macros.yml# Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. {% set config = pillar['config'] %} {% macro send_enter(name) -%} event_{{ name }}_enter: module.run: - event.send: - tag: yomi/{{ name }}/enter # - with_grains: [id, hwaddr_interfaces] {%- endmacro %} {% macro send_success(state, name) -%} event_{{ name }}_success: module.run: - event.send: - tag: yomi/{{ name }}/success # - with_grains: [id, hwaddr_interfaces] - onchanges: - {{ state }}: {{ name }} {%- endmacro %} {% macro send_fail(state, name) -%} event_{{ name }}_fail: module.run: - event.send: - tag: yomi/{{ name }}/fail # - with_grains: [id, hwaddr_interfaces] - onfail: - {{ state }}: {{ name }} {%- endmacro %} {% macro log(state, name) -%} {% if config.get('events', True) %} {{ send_enter(name) }} {{ send_success(state, name) }} {{ send_fail(state, name) }} {% endif %} {%- endmacro %} 07070100000032000081A40000000000000000000000016130D1CF00000018000000000000000000000000000000000000002F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/top.slsbase: '*': - yomi 07070100000033000041ED0000000000000000000000076130D1CF00000000000000000000000000000000000000000000002C00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi07070100000034000081A40000000000000000000000016130D1CF0000016C000000000000000000000000000000000000004000000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/_default_target.sls{% import 'macros.yml' as macros %} {% set config = pillar['config'] %} {% set target = config.get('target', 'multi-user.target') %} {{ macros.log('cmd', 'systemd_set_target') }} systemd_set_target: cmd.run: - name: systemctl set-default {{ target }} - unless: readlink -f /mnt/etc/systemd/system/default.target | grep -q {{ target }} - root: /mnt 07070100000035000081A40000000000000000000000016130D1CF000004A0000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/_firstboot.sls{% import 'macros.yml' as macros %} {% set config = pillar['config'] %} # We execute the systemctl call inside the chroot, so we can guarantee # that will work on containers {{ macros.log('module', 'systemd_firstboot') }} systemd_firstboot: module.run: - chroot.call: - root: /mnt - function: service.firstboot - locale: {{ config.get('locale', 'en_US.utf8') }} {% if config.get('locale_messages') %} - locale_message: {{ config['locale_messages'] }} {% endif %} - keymap: {{ config.get('keymap', 'us') }} - timezone: {{ config.get('timezone', 'UTC') }} {% if config.get('hostname') %} - hostname: {{ config['hostname'] }} {% endif %} {% if config.get('machine_id') %} - machine_id: {{ config['machine_id'] }} {% endif %} - creates: - /mnt/etc/hostname - /mnt/etc/locale.conf - /mnt/etc/localtime - /mnt/etc/machine-id - /mnt/etc/vconsole.conf {% if not config.get('machine_id') %} {{ macros.log('module', 'create_machine-id') }} create_machine-id: module.run: - file.copy: - src: /etc/machine-id - dst: /mnt/etc/machine-id - remove_existing: yes {% endif %} 07070100000036000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003700000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/bootloader07070100000037000081A40000000000000000000000016130D1CF00000327000000000000000000000000000000000000004900000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/bootloader/grub2_install.sls{% import 'macros.yml' as macros %} {% set bootloader = pillar['bootloader'] %} {% set arch = {'aarch64': 'arm64'}.get(grains['cpuarch'], grains['cpuarch'])%} {{ macros.log('cmd', 'grub2_install') }} grub2_install: cmd.run: {% if grains['efi'] %} {% if grains['efi-secure-boot'] %} - name: shim-install --config-file=/boot/grub2/grub.cfg {% else %} - name: grub2-install --target={{ arch }}-efi --efi-directory=/boot/efi --bootloader-id=GRUB {% endif %} - creates: /mnt/boot/efi/EFI/GRUB {% else %} - name: grub2-install --force {{ bootloader.device }} - creates: /mnt/boot/grub2/i386-pc/normal.mod {% endif %} {% if pillar.get('lvm') %} - binds: [/run] - env: - LVM_SUPPRESS_FD_WARNINGS: 1 {% endif %} - root: /mnt - require: - cmd: grub2_mkconfig 07070100000038000081A40000000000000000000000016130D1CF0000086B000000000000000000000000000000000000004A00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/bootloader/grub2_mkconfig.sls{% import 'macros.yml' as macros %} {% set config = pillar['config'] %} {% set bootloader = pillar['bootloader'] %} {% if config.get('snapper') %} include: {% if config.get('snapper') %} - ..storage.snapper.grub2_mkconfig {% endif %} {% endif %} {% if grains['efi'] and grains['cpuarch'] != 'aarch64' %} {{ macros.log('file', 'config_grub2_efi') }} config_grub2_efi: file.append: - name: /mnt/etc/default/grub - text: GRUB_USE_LINUXEFI="true" {% endif %} {% if bootloader.get('theme') %} {{ macros.log('file', 'config_grub2_theme') }} config_grub2_theme: file.append: - name: /mnt/etc/default/grub - text: - GRUB_TERMINAL="{{ bootloader.get('terminal', 'gfxterm') }}" - GRUB_GFXMODE="{{ bootloader.get('gfxmode', 'auto') }}" - GRUB_BACKGROUND= # - GRUB_THEME="/boot/grub2/themes/openSUSE/theme.txt" {% endif %} {{ macros.log('file', 'config_grub2_resume') }} config_grub2_resume: file.append: - name: /mnt/etc/default/grub - text: - GRUB_TIMEOUT={{ bootloader.get('timeout', 8) }} {% if not pillar.get('lvm') %} - GRUB_DEFAULT="saved" # - GRUB_SAVEDEFAULT="true" {% endif %} {% set serial_command = bootloader.get('serial_command')%} {{ macros.log('file', 'config_grub2_config') }} config_grub2_config: file.append: - name: /mnt/etc/default/grub - text: - GRUB_CMDLINE_LINUX_DEFAULT="{{ bootloader.get('kernel', 'splash=silent quiet') }}" - GRUB_DISABLE_OS_PROBER="{{ true if bootloader.get('disable_os_prober') else false }}" {% if serial_command %} - GRUB_TERMINAL="serial" - GRUB_SERIAL_COMMAND="{{ serial_command }}" {% endif %} {{ macros.log('cmd', 'grub2_set_default') }} grub2_set_default: cmd.run: - name: (source /etc/os-release; grub2-set-default "${PRETTY_NAME}") - root: /mnt - onlyif: "[ -e /mnt/etc/os-release ]" - watch: - file: /mnt/etc/default/grub {{ macros.log('cmd', 'grub2_mkconfig') }} grub2_mkconfig: cmd.run: - name: grub2-mkconfig -o /boot/grub2/grub.cfg - root: /mnt {% if pillar.get('lvm') %} - binds: [/run] {% endif %} - watch: - file: /mnt/etc/default/grub 07070100000039000081A40000000000000000000000016130D1CF00000055000000000000000000000000000000000000004000000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/bootloader/init.sls{% set config = pillar['config'] %} include: - .grub2_mkconfig - .grub2_install 0707010000003A000081A40000000000000000000000016130D1CF00000325000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/bootloader/software.sls{% import 'macros.yml' as macros %} {% set bootloader = pillar['bootloader'] %} {% set arch = {'aarch64': 'arm64'}.get(grains['cpuarch'], grains['cpuarch'])%} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {{ macros.log('pkg', 'install_grub2') }} install_grub2: pkg.installed: - pkgs: - grub2 {% if bootloader.get('theme') %} - grub2-branding {% endif %} {% if grains['efi'] %} - grub2-{{ arch }}-efi {% if grains['efi-secure-boot'] %} - shim {% endif %} {% endif %} - resolve_capabilities: yes {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - root: /mnt - require: - mount: mount_/mnt 0707010000003B000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003300000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/chroot0707010000003C000081A40000000000000000000000016130D1CF0000015F000000000000000000000000000000000000003D00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/chroot/mount.sls{% import 'macros.yml' as macros %} {% for fstype, fs_file in (('devtmpfs', '/mnt/dev'), ('proc', '/mnt/proc'), ('sysfs', '/mnt/sys')) %} {{ macros.log('mount', 'mount_' ~ fs_file) }} mount_{{ fs_file }}: mount.mounted: - name: {{ fs_file }} - device: {{ fstype }} - fstype: {{ fstype }} - mkmnt: yes - persist: no {% endfor %} 0707010000003D000081A40000000000000000000000016130D1CF00000131000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/chroot/post_install.sls{% import 'macros.yml' as macros %} {{ macros.log('module', 'unfreeze_chroot') }} unfreeze_chroot: module.run: - freezer.restore: - name: yomi-chroot - clean: True - includes: [pattern] - root: /mnt - onlyif: "[ -e /var/cache/salt/minion/freezer/yomi-chroot-pkgs.yml ]" 0707010000003E000081A40000000000000000000000016130D1CF000002E6000000000000000000000000000000000000004000000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/chroot/software.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {{ macros.log('module', 'freeze_chroot') }} freeze_chroot: module.run: - freezer.freeze: - name: yomi-chroot - includes: [pattern] - root: /mnt - unless: "[ -e /var/cache/salt/minion/freezer/yomi-chroot-pkgs.yml ]" {{ macros.log('pkg', 'install_python3-base') }} install_python3-base: pkg.installed: - name: python3-base - resolve_capabilities: yes {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - root: /mnt - require: - mount: mount_/mnt 0707010000003F000081A40000000000000000000000016130D1CF00000104000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/chroot/umount.sls{% import 'macros.yml' as macros %} {% for fs_file in ('/mnt/sys', '/mnt/proc', '/mnt/dev' ) %} {{ macros.log('mount', 'umount_' ~ fs_file) }} umount_{{ fs_file }}: mount.unmounted: - name: {{ fs_file }} - requires: mount_{{ fs_file }} {% endfor %} 07070100000040000081A40000000000000000000000016130D1CF000001E6000000000000000000000000000000000000003500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/init.sls{% set filesystems = pillar['filesystems'] %} {% set ns = namespace(installed=False) %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {% if salt.cmd.run('findmnt --list --noheadings --output SOURCE /') == device %} {% set ns.installed = True %} {% endif %} {% endif %} {% endfor %} {% if not ns.installed %} include: - .storage - .software - .users - .bootloader - .services - .post_install - .reboot {% endif %} 07070100000041000081A40000000000000000000000016130D1CF00000061000000000000000000000000000000000000003D00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/post_install.slsinclude: - .chroot.post_install - ._firstboot - ._default_target - .storage.post_install 07070100000042000081A40000000000000000000000016130D1CF00000439000000000000000000000000000000000000003700000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/reboot.sls{% import 'macros.yml' as macros %} {% set config = pillar['config'] %} {% set reboot = config.get('reboot', True) %} {% if reboot == 'kexec' %} {{ macros.log('cmd', 'grub_command_line') }} grub_command_line: cmd.run: - name: grep -m 1 -E '^[[:space:]]*linux(efi)?[[:space:]]+[^[:space:]]+vmlinuz.*$' /mnt/boot/grub2/grub.cfg | cut -d ' ' -f 2-3 > /tmp/command_line - create: /tmp/command_line {{ macros.log('cmd', 'prepare_kexec') }} prepare_kexec: cmd.run: - name: kexec -a -l /mnt/boot/vmlinuz --initrd=/mnt/boot/initrd --command-line="$(cat /tmp/command_line)" - onlyif: "[ -e /tmp/command_line ]" {{ macros.log('cmd', 'execute_kexec') }} execute_kexec: cmd.run: - name: systemctl kexec {% elif reboot == 'halt' %} {{ macros.log('module', 'halt') }} halt: module.run: - system.halt: {% elif reboot == 'shutdown' %} {{ macros.log('module', 'shutdown') }} shutdown: module.run: - system.shutdown: {% elif reboot == 'yes' or reboot == True %} {{ macros.log('module', 'reboot') }} reboot: module.run: - system.reboot: {% endif %} 07070100000043000041ED0000000000000000000000036130D1CF00000000000000000000000000000000000000000000003500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services07070100000044000081A40000000000000000000000016130D1CF000003C6000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services/init.sls{% import 'macros.yml' as macros %} {% set services = pillar.get('services', {}) %} include: - .network {% if pillar.get('salt-minion') %} - .salt-minion {% endif %} {% for service in services.get('enabled', []) %} # We execute the systemctl call inside the chroot, so we can guarantee # that will work on containers {{ macros.log('module', 'enable_service_' ~ service) }} enable_service_{{ service }}: module.run: - chroot.call: - root: /mnt - function: service.enable - name: {{ service }} - unless: systemctl --root=/mnt --quiet is-enabled {{ service }} 2> /dev/null {% endfor %} {% for service in services.get('disabled', []) %} {{ macros.log('module', 'disable_service_' ~ service) }} disable_service_{{ service }}: module.run: - chroot.call: - root: /mnt - function: service.disable - name: {{ service }} - onlyif: systemctl --root=/mnt --quiet is-enabled {{ service }} 2> /dev/null {% endfor %} 07070100000045000081A40000000000000000000000016130D1CF000007CB000000000000000000000000000000000000004100000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services/network.sls{% import 'macros.yml' as macros %} {% set networks = pillar.get('networks') %} {% if networks %} {% for network in networks %} create_ifcfg_{{ network.interface }}: file.append: - name: /mnt/etc/sysconfig/network/ifcfg-{{ network.interface }} - text: | NAME='' BOOTPROTO='dhcp' STARTMODE='auto' ZONE='' {% endfor %} {% else %} # This assume that the image used for deployment is under a # predictable network interface name, like Tumbleweed. For SLE, boot # the image with `net.ifnames=1` {% set interfaces = salt.network.interfaces() %} {% set interfaces_except_lo = interfaces | select('!=', 'lo') %} {% for interface in interfaces_except_lo %} {{ macros.log('file', 'create_ifcfg_' ~ interface) }} create_ifcfg_{{ interface }}: file.append: - name: /mnt/etc/sysconfig/network/ifcfg-{{ interface }} - text: | NAME='' BOOTPROTO='dhcp' STARTMODE='auto' ZONE='' - unless: "[ -e /mnt/usr/lib/udev/rules.d/75-persistent-net-generator.rules ]" {{ macros.log('file', 'create_ifcfg_eth' ~ loop.index0) }} create_ifcfg_eth{{ loop.index0 }}: file.append: - name: /mnt/etc/sysconfig/network/ifcfg-eth{{ loop.index0 }} - text: | NAME='' BOOTPROTO='dhcp' STARTMODE='auto' ZONE='' - onlyif: "[ -e /mnt/usr/lib/udev/rules.d/75-persistent-net-generator.rules ]" {{ macros.log('cmd', 'write_net_rules_eth' ~ loop.index0) }} write_net_rules_eth{{ loop.index0 }}: cmd.run: - name: /usr/lib/udev/write_net_rules - env: - INTERFACE: eth{{ loop.index0 }} - MATCHADDR: "{{ interfaces[interface].hwaddr }}" - root: /mnt - onlyif: "[ -e /mnt/usr/lib/udev/rules.d/75-persistent-net-generator.rules ]" {% endfor %} {% endif %} {{ macros.log('file', 'dhcp_hostname') }} dhcp_hostname: file.append: - name: /mnt/etc/sysconfig/network/dhcp - text: - DHCLIENT_SET_HOSTNAME="yes" - WRITE_HOSTNAME_TO_HOSTS="no" 07070100000046000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000004100000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services/salt-minion07070100000047000081A40000000000000000000000016130D1CF0000036A000000000000000000000000000000000000004A00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services/salt-minion/init.sls{% import 'macros.yml' as macros %} {% set salt_minion = pillar['salt-minion'] %} {% if salt_minion.get('config') %} {{ macros.log('module', 'synchronize_salt-minion_etc') }} synchronize_salt-minion_etc: module.run: - file.copy: - src: /etc/salt - dst: /mnt/etc/salt - recurse: yes - remove_existing: yes - unless: "[ -e /mnt/etc/salt/pki/minion/minion.pem ]" {{ macros.log('module', 'synchronize_salt-minion_var') }} synchronize_salt-minion_var: module.run: - file.copy: - src: /var/cache/salt - dst: /mnt/var/cache/salt - recurse: yes - remove_existing: yes - unless: "[ -e /mnt/var/cache/salt/minion/extmods ]" {{ macros.log('file', 'clean_salt-minion_var') }} clean_salt-minion_var: file.tidied: - name: /mnt/var/cache/salt/minion - matches: - ".*\\.pyc" - "\\d+" {% endif %} 07070100000048000081A40000000000000000000000016130D1CF000001CF000000000000000000000000000000000000004E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services/salt-minion/software.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {{ macros.log('pkg', 'install_salt-minion') }} install_salt-minion: pkg.installed: - name: salt-minion {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - root: /mnt - require: - mount: mount_/mnt 07070100000049000081A40000000000000000000000016130D1CF00000085000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/services/software.sls{% if pillar.get('salt-minion') %} include: {% if pillar.get('salt-minion') %} - .salt-minion.software {% endif %} {% endif %} 0707010000004A000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software0707010000004B000081A40000000000000000000000016130D1CF000002A1000000000000000000000000000000000000003F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software/image.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% set software = pillar['software'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('module', 'dump_image_into_' ~ device) }} dump_image_into_{{ device }}: images.dumped: - name: {{ software.image.url }} - device: {{ device }} {% for checksum_type in ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512') %} {% if checksum_type in software.image %} - checksum_type: {{ checksum_type }} - checksum: {{ software.image[checksum_type] or '' }} {% endif %} {% endfor %} {% endif %} {% endfor %} 0707010000004C000081A40000000000000000000000016130D1CF000001C4000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software/init.sls{% set software = pillar['software'] %} include: {# TODO: Remove the double check (SumaForm bug) #} {% if software.get('image', {}).get('url') %} - .image - ..storage.fstab - ..storage.mount {% endif %} - .repository - .software {% if pillar.get('suseconnect', {}).get('config', {}).get('regcode') %} - .suseconnect {% endif %} - ..storage.software - ..bootloader.software - ..services.software - ..chroot.software - .recreatedb 0707010000004D000081A40000000000000000000000016130D1CF000001F8000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software/recreatedb.sls{% import 'macros.yml' as macros %} {{ macros.log('cmd', 'rpm_exportdb') }} rpm_exportdb: cmd.run: - name: rpmdb --root /mnt --exportdb > /mnt/tmp/exportdb - creates: /mnt/tmp/exportdb {{ macros.log('file', 'clean_usr_lib_sysimage_rpm') }} clean_usr_lib_sysimage_rpm: file.absent: - name: /mnt/usr/lib/sysimage/rpm {{ macros.log('cmd', 'rpm_importdb') }} rpm_importdb: cmd.run: - name: rpmdb --importdb < /tmp/exportdb - root: /mnt - onchanges: - cmd: rpm_exportdb 0707010000004E000081A40000000000000000000000016130D1CF0000089C000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software/repository.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {% if software_config.get('transfer') %} {{ macros.log('module', 'transfer_repositories') }} migrate_repositories: pkgrepo.migrated: - name: /mnt - keys: yes {% for cert_dir in ['/usr/share/pki/trust/anchors', '/usr/share/pki/trust/blacklist', '/etc/pki/trust/anchors', '/etc/pki/trust/blacklist'] %} migrate_{{ cert_dir }}: module.run: - file.copy: - src: {{ cert_dir }} - dst: /mnt{{ cert_dir }} - recurse: yes - remove_existing: yes - unless: "[ -e /mnt{{ cert_dir }} ]" {% endfor %} {% endif %} # TODO: boo#1178910 - This zypper bug creates /var/lib/rpm and # /usr/lib/sysimage/rpm independently, and not linked together {{ macros.log('file', 'create_usr_lib_sysimage_rpm') }} create_usr_lib_sysimage_rpm: file.directory: - name: /mnt/usr/lib/sysimage/rpm - makedirs: yes {{ macros.log('file', 'symlink_var_lib_rpm') }} symlink_var_lib_rpm: file.symlink: - name: /mnt/var/lib/rpm - target: ../../usr/lib/sysimage/rpm - makedirs: yes {% for alias, repository in software.get('repositories', {}).items() %} {% if repository is mapping %} {% set url = repository['url'] %} {% else %} {% set url = repository %} {% set repository = {} %} {% endif %} {{ macros.log('pkgrepo', 'add_repository_' ~ alias) }} add_repository_{{ alias }}: pkgrepo.managed: - baseurl: {{ url }} - name: {{ alias }} {% if repository.get('name') %} - humanname: {{ repository.name }} {% endif %} - enabled: {{ repository.get('enabled', software_config.get('enabled', 'yes')) }} - refresh: {{ repository.get('refresh', software_config.get('refresh', 'yes')) }} - priority: {{ repository.get('priority', 0) }} - gpgcheck: {{ repository.get('gpgcheck', software_config.get('gpgcheck', 'yes')) }} - gpgautoimport: {{ repository.get('gpgautoimport', software_config.get('gpgautoimport', 'yes')) }} - cache: {{ repository.get('cache', software_config.get('cache', 'no')) }} - root: /mnt - require: - mount: mount_/mnt {% endfor %} 0707010000004F000081A40000000000000000000000016130D1CF00000441000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software/software.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {% if software_config.get('minimal') %} {{ macros.log('file', 'config_zypp_minimal_host') }} config_zypp_minimal_host: file.append: - name: /etc/zypp/zypp.conf - text: - solver.onlyRequires = true - rpm.install.excludedocs = yes - multiversion = {% endif %} {% if software.get('packages') %} {{ macros.log('pkg', 'install_packages') }} install_packages: pkg.installed: - pkgs: {{ software.packages }} {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - includes: [product, pattern] - root: /mnt {% endif %} {% if software_config.get('minimal') %} {{ macros.log('file', 'config_zypp_minimal') }} config_zypp_minimal: file.append: - name: /mnt/etc/zypp/zypp.conf - text: - solver.onlyRequires = true - rpm.install.excludedocs = yes - multiversion = {% endif %} 07070100000050000081A40000000000000000000000016130D1CF0000070A000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/software/suseconnect.sls{% import 'macros.yml' as macros %} {% set suseconnect = pillar['suseconnect'] %} {% set suseconnect_config = suseconnect['config'] %} {{ macros.log('suseconnect', 'register_product') }} register_product: suseconnect.registered: - regcode: {{ suseconnect_config['regcode'] }} {% if suseconnect_config.get('email') %} - email: {{ suseconnect_config['email'] }} {% endif %} {% if suseconnect_config.get('url') %} - url: {{ suseconnect_config['url'] }} {% endif %} - root: /mnt - require: - mount: mount_/mnt {% for product in suseconnect.get('products', []) %} {% set regcode = suseconnect_config['regcode'] %} {% if product is mapping %} {% set regcode = product.get('regcode', regcode) %} {% set product = product['name'] %} {% endif %} {% if 'version' in suseconnect_config and 'arch' in suseconnect_config %} {% if suseconnect_config['version'] not in product %} {% set product = '%s/%s/%s'|format(product, suseconnect_config['version'], suseconnect_config['arch']) %} {% endif %} {% endif %} {{ macros.log('suseconnect', 'register_' ~ product) }} register_{{ product }}: suseconnect.registered: - regcode: {{ regcode }} - product: {{ product }} {% if suseconnect_config.get('email') %} - email: {{ suseconnect_config['email'] }} {% endif %} {% if suseconnect_config.get('url') %} - url: {{ suseconnect_config['url'] }} {% endif %} - root: /mnt - require: - mount: mount_/mnt {% endfor %} {% if suseconnect.get('packages') %} {{ macros.log('pkg', 'install_packages_product') }} install_packages_product: pkg.installed: - pkgs: {{ suseconnect.packages }} - no_recommends: yes - includes: [product, pattern] - root: /mnt - require: - suseconnect: register_product {% endif %} 07070100000051000041ED0000000000000000000000076130D1CF00000000000000000000000000000000000000000000003400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage07070100000052000081A40000000000000000000000016130D1CF0000045F000000000000000000000000000000000000004300000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/_partition.sls{% import 'macros.yml' as macros %} {% set partitions = salt.partmod.prepare_partition_data(pillar['partitions']) %} {% set is_uefi = grains['efi'] %} {% for device, device_info in partitions.items() if filter(device) %} {{ macros.log('partitioned', 'create_disk_label_' ~ device) }} create_disk_label_{{ device }}: partitioned.labeled: - name: {{ device }} - label: {{ device_info.label }} {% if device_info.pmbr_boot %} {{ macros.log('partitioned', 'set_pmbr_boot_' ~ device) }} set_pmbr_boot_{{ device }}: partitioned.disk_set: - name: {{ device }} - flag: pmbr_boot - enabled: yes {% endif %} {% for partition in device_info.get('partitions', []) %} {{ macros.log('partitioned', 'create_partition_' ~ partition.part_id) }} create_partition_{{ partition.part_id }}: partitioned.mkparted: - name: {{ partition.part_id }} - part_type: {{ partition.part_type }} - fs_type: {{ partition.fs_type }} - start: {{ partition.start }} - end: {{ partition.end }} {% if partition.flags %} - flags: {{ partition.flags }} {% endif %} {% endfor %} {% endfor %} 07070100000053000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003A00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/btrfs07070100000054000081A40000000000000000000000016130D1CF000005ED000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/btrfs/fstab.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('mount', 'mount_btrfs_fstab') }} mount_btrfs_fstab: mount.mounted: - name: /mnt - device: {{ device }} - fstype: {{ info.filesystem }} - persist: no {% endif %} {% endfor %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.get('subvolumes') %} {% set prefix = info.subvolumes.get('prefix', '') %} {% for subvol in info.subvolumes.subvolume %} {% set fs_file = '/'|path_join(subvol.path) %} {% set fs_mntops = 'subvol=%s'|format('/'|path_join(prefix, subvol.path)) %} {% if not subvol.get('copy_on_write', True) %} {# TODO(aplanas) nodatacow seems optional if chattr was used #} {% set fs_mntops = fs_mntops ~ ',nodatacow' %} {% endif %} {{ macros.log('mount', 'add_fstab' ~ '_' ~ fs_file) }} add_fstab_{{ fs_file }}: mount.fstab_present: - name: {{ device }} - fs_file: {{ fs_file }} - fs_vfstype: {{ info.filesystem }} - fs_mntops: {{ fs_mntops }} - fs_freq: 0 - fs_passno: 0 - mount_by: uuid - mount: no - not_change: yes - config: /mnt/etc/fstab - require: - mount: mount_btrfs_fstab {% endfor %} {% endif %} {% endfor %} {{ macros.log('mount', 'umount_btrfs_fstab') }} umount_btrfs_fstab: mount.unmounted: - name: /mnt - requires: mount_btrfs_fstab 07070100000055000081A40000000000000000000000016130D1CF0000034F000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/btrfs/mount.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.get('subvolumes') %} {% set prefix = info.subvolumes.get('prefix', '') %} {% for subvol in info.subvolumes.subvolume %} {% set fs_file = '/mnt'|path_join(subvol.path) %} {% set fs_mntops = 'subvol=%s'|format('/'|path_join(prefix, subvol.path)) %} {% if not subvol.get('copy_on_write', True) %} {% set fs_mntops = fs_mntops ~ ',nodatacow' %} {% endif %} {{ macros.log('mount', 'mount_' ~ fs_file) }} mount_{{ fs_file }}: mount.mounted: - name: {{ fs_file }} - device: {{ device }} - fstype: {{ info.filesystem }} - mkmnt: yes - opts: {{ fs_mntops }} - persist: no {% endfor %} {% endif %} {% endfor %} 07070100000056000081A40000000000000000000000016130D1CF000001C4000000000000000000000000000000000000004B00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/btrfs/post_install.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and 'ro' in info.get('options', []) %} {{ macros.log('btrfs', 'set_property_ro_' ~ info.mountpoint) }} set_property_ro_{{ info.mountpoint }}: btrfs.properties: - name: {{ info.mountpoint }} - device: {{ device }} - use_default: yes - ro: yes {% endif %} {% endfor %} 07070100000057000081A40000000000000000000000016130D1CF0000042B000000000000000000000000000000000000004800000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/btrfs/subvolume.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.get('subvolumes') %} {# TODO(aplanas) is prefix optional? #} {% set prefix = info.subvolumes.get('prefix', '') %} {% if prefix %} {{ macros.log('btrfs', 'subvol_create_' ~ device ~ '_prefix') }} subvol_create_{{ device }}_prefix: btrfs.subvolume_created: - name: '{{ prefix }}' - device: {{ device }} - set_default: yes - force_set_default: no {% endif %} {% for subvol in info.subvolumes.subvolume %} {% if prefix %} {% set path = prefix|path_join(subvol.path) %} {% else %} {% set path = subvol.path %} {% endif %} {{ macros.log('btrfs', 'subvol_create_' ~ device ~ '_' ~ subvol.path) }} subvol_create_{{ device }}_{{ subvol.path }}: btrfs.subvolume_created: - name: '{{ path }}' - device: {{ device }} - copy_on_write: {{ subvol.get('copy_on_write', True) }} {% endfor %} {% endif %} {% endfor %} 07070100000058000081A40000000000000000000000016130D1CF00000228000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/btrfs/umount.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.get('subvolumes') %} {% set prefix = info.subvolumes.get('prefix', '') %} {% for subvol in info.subvolumes.subvolume %} {% set fs_file = '/mnt'|path_join(subvol.path) %} {{ macros.log('mount', 'umount_' ~ fs_file) }} umount_{{ fs_file }}: mount.unmounted: - name: {{ fs_file }} - requires: mount_{{ fs_file }} {% endfor %} {% endif %} {% endfor %} 07070100000059000081A40000000000000000000000016130D1CF000002FE000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/create_fstab.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('mount', 'mount_create_fstab') }} mount_create_fstab: mount.mounted: - name: /mnt - device: {{ device }} - fstype: {{ info.filesystem }} - persist: no {{ macros.log('file', 'create_fstab') }} create_fstab: file.managed: - name: /mnt/etc/fstab - user: root - group: root - mode: 644 - makedirs: yes - dir_mode: 755 - replace: no - requires: mount_create_fstab {{ macros.log('mount', 'umount_create_fstab') }} umount_create_fstab: mount.unmounted: - name: /mnt - requires: mount_create_fstab {% endif %} {% endfor %} 0707010000005A000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003B00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/device0707010000005B000081A40000000000000000000000016130D1CF0000047F000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/device/fstab.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('mount', 'mount_device_fstab') }} mount_device_fstab: mount.mounted: - name: /mnt - device: {{ device }} - fstype: {{ info.filesystem }} - persist: no {% endif %} {% endfor %} {% for device, info in filesystems.items() %} {% set fs_file = 'swap' if info.filesystem == 'swap' else info.mountpoint %} {{ macros.log('mount', 'add_fstab_' ~ fs_file) }} add_fstab_{{ fs_file }}: mount.fstab_present: - name: {{ device }} - fs_file: {{ fs_file }} - fs_vfstype: {{ info.filesystem }} - fs_mntops: {{ ','.join(info.get('options', ['defaults'])) }} - fs_freq: 0 - fs_passno: 0 {% if not salt.filters.is_lvm(device) %} - mount_by: uuid {% endif %} - mount: no - not_change: yes - config: /mnt/etc/fstab - require: - mount: mount_device_fstab {% endfor %} {{ macros.log('mount', 'umount_device_fstab') }} umount_device_fstab: mount.unmounted: - name: /mnt - requires: mount_device_fstab 0707010000005C000081A40000000000000000000000016130D1CF0000033A000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/device/mount.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('mount', 'mount_/mnt') }} mount_/mnt: mount.mounted: - name: /mnt - device: {{ device }} - fstype: {{ info.filesystem }} - persist: no {% endif %} {% endfor %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') and info.mountpoint != '/' %} {% set fs_file = '/mnt'|path_join(info.mountpoint[1:] if info.mountpoint.startswith('/') else info.mountpoint) %} {{ macros.log('mount', 'mount_' ~ fs_file) }} mount_{{ fs_file }}: mount.mounted: - name: {{ fs_file }} - device: {{ device }} - fstype: {{ info.filesystem }} - mkmnt: yes - persist: no {% endif %} {% endfor %} 0707010000005D000081A40000000000000000000000016130D1CF000002CE000000000000000000000000000000000000004600000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/device/umount.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') and info.mountpoint != '/' %} {% set fs_file = '/mnt'|path_join(info.mountpoint[1:] if info.mountpoint.startswith('/') else info.mountpoint) %} {{ macros.log('mount', 'umount_' ~ fs_file) }} umount_{{ fs_file }}: mount.unmounted: - name: {{ fs_file }} - requires: mount_{{ fs_file }} {% endif %} {% endfor %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('mount', 'umount_/mnt') }} umount_/mnt: mount.unmounted: - name: /mnt - requires: mount_/mnt {% endif %} {% endfor %} 0707010000005E000081A40000000000000000000000016130D1CF000001A5000000000000000000000000000000000000003F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/format.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {{ macros.log('formatted', 'mkfs_partition_' ~ device) }} mkfs_partition_{{ device }}: formatted.formatted: - name: {{ device }} - fs_type: {{ info.filesystem }} {% if info.filesystem in ('fat', 'vfat') and info.get('fat') %} - fat: {{ info.fat }} {% endif %} {% endfor %} 0707010000005F000081A40000000000000000000000016130D1CF000000A1000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/fstab.sls{% set config = pillar['config'] %} include: - .create_fstab - .device.fstab - .btrfs.fstab {% if config.get('snapper') %} - .snapper.fstab {% endif %} 07070100000060000081A40000000000000000000000016130D1CF000000FA000000000000000000000000000000000000003D00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/init.sls{% set software = pillar['software'] %} include: - .partition - .raid - .volumes - .format - .subvolumes {# TODO: Remove the double check (SumaForm bug) #} {% if not software.get('image', {}).get('url') %} - .fstab - .mount {% endif %}07070100000061000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003800000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/lvm07070100000062000081A40000000000000000000000016130D1CF000001BA000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/lvm/software.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {{ macros.log('pkg', 'install_lvm2') }} install_lvm2: pkg.installed: - name: lvm2 {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - root: /mnt - require: - mount: mount_/mnt 07070100000063000081A40000000000000000000000016130D1CF0000055D000000000000000000000000000000000000004300000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/lvm/volume.sls{% import 'macros.yml' as macros %} {% set lvm = pillar.get('lvm', {}) %} {% for group, group_info in lvm.items() %} {% set devices = [] %} {% for device in group_info['devices'] %} {% set info = {} %} {# We can store the device information inside a dict #} {% if device is mapping %} {% set info = device %} {% set device = device['name'] %} {% endif %} {% do devices.append(device) %} {{ macros.log('lvm', 'create_physical_volume_' ~ device) }} create_physical_volume_{{ device }}: lvm.pv_present: - name: {{ device }} {% for key, value in info.items() if key != 'name' %} - {{ key }}: {{ value }} {% endfor %} {% endfor %} {{ macros.log('lvm', 'create_virtual_group_' ~ group) }} create_virtual_group_{{ group }}: lvm.vg_present: - name: {{ group }} - devices: [{{ ', '.join(devices) }}] {% for key, value in group_info.items() if key not in ('devices', 'volumes') %} - {{ key }}: {{ value }} {% endfor %} {% for volume in group_info['volumes'] %} {{ macros.log('lvm', 'create_logical_volume_' ~ volume['name']) }} create_logical_volume_{{ volume['name'] }}: lvm.lv_present: - name: {{ volume['name'] }} - vgname: {{ group }} {% for key, value in volume.items() if key not in ('name', 'vgname') %} - {{ key }}: {{ value }} {% endfor %} {% endfor %} {% endfor %} 07070100000064000081A40000000000000000000000016130D1CF000000A1000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/mount.sls{% set config = pillar['config'] %} include: - .device.mount - .btrfs.mount {% if config.get('snapper') %} - .snapper.mount {% endif %} - ..chroot.mount07070100000065000081A40000000000000000000000016130D1CF0000004B000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/partition.sls{% set filter=salt.filters.is_not_raid %} {% include './_partition.sls' %} 07070100000066000081A40000000000000000000000016130D1CF000000CB000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/post_install.sls{% set config = pillar['config'] %} include: {% if config.get('snapper') %} - .snapper.post_install {% endif %} - .btrfs.post_install {% if not config.get('reboot', True) %} - .umount {% endif %} 07070100000067000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003900000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/raid07070100000068000081A40000000000000000000000016130D1CF00000023000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/raid/init.slsinclude: - .mdadm - .partition 07070100000069000081A40000000000000000000000016130D1CF000001AD000000000000000000000000000000000000004300000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/raid/mdadm.sls{% import 'macros.yml' as macros %} {% set raid = pillar.get('raid', {}) %} {% for device, info in raid.items() %} {{ macros.log('raid', 'create_raid_' ~ device) }} create_raid_{{ device }}: raid.present: - name: {{ device }} - level: {{ info.level }} - devices: {{ info.devices }} {% for key, value in info.items() if key not in ('level', 'devices') %} - {{ key }}: {{ value }} {% endfor %} {% endfor %} 0707010000006A000081A40000000000000000000000016130D1CF00000048000000000000000000000000000000000000004700000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/raid/partition.sls{% set filter=salt.filters.is_raid %} {% include '../_partition.sls' %} 0707010000006B000081A40000000000000000000000016130D1CF000001D2000000000000000000000000000000000000004600000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/raid/software.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {{ macros.log('pkg', 'install_raid') }} install_raid: pkg.installed: - pkgs: - mdadm - dmraid {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - root: /mnt - require: - mount: mount_/mnt 0707010000006C000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper0707010000006D000081A40000000000000000000000016130D1CF00000526000000000000000000000000000000000000004600000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/fstab.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.get('mountpoint') == '/' %} {{ macros.log('mount', 'mount_snapper_fstab') }} mount_snapper_fstab: mount.mounted: - name: /mnt - device: {{ device }} - fstype: {{ info.filesystem }} - persist: no {% endif %} {% endfor %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.mountpoint == '/' %} {% set prefix = info.subvolumes.get('prefix', '') %} {% set fs_file = '/'|path_join('.snapshots') %} {% set fs_mntops = 'subvol=%s'|format('/'|path_join(prefix, '.snapshots')) %} {{ macros.log('mount', 'add_fstab_' ~ fs_file) }} add_fstab_{{ fs_file }}: mount.fstab_present: - name: {{ device }} - fs_file: {{ fs_file }} - fs_vfstype: {{ info.filesystem }} - fs_mntops: {{ fs_mntops }} - fs_freq: 0 - fs_passno: 0 {% if not salt.filters.is_lvm(device) %} - mount_by: uuid {% endif %} - mount: no - not_change: yes - config: /mnt/etc/fstab - require: - mount: mount_snapper_fstab {% endif %} {% endfor %} {{ macros.log('mount', 'umount_snapper_fstab') }} umount_snapper_fstab: mount.unmounted: - name: /mnt - requires: mount_snapper_fstab 0707010000006E000081A40000000000000000000000016130D1CF000000CC000000000000000000000000000000000000004F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/grub2_mkconfig.sls{% import 'macros.yml' as macros %} {{ macros.log('file', 'config_snapper_grub2') }} config_snapper_grub2: file.append: - name: /mnt/etc/default/grub - text: SUSE_BTRFS_SNAPSHOT_BOOTING="true" 0707010000006F000081A40000000000000000000000016130D1CF0000028B000000000000000000000000000000000000004600000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/mount.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.mountpoint == '/' %} {% set prefix = info.subvolumes.get('prefix', '') %} {% set fs_mntops = 'subvol=%s'|format('/'|path_join(prefix, '.snapshots')) %} {% set fs_file = '/mnt'|path_join('.snapshots') %} {{ macros.log('mount', 'mount_' ~ fs_file) }} mount_{{ fs_file }}: mount.mounted: - name: {{ fs_file }} - device: {{ device }} - fstype: {{ info.filesystem }} - mkmnt: no - opts: {{ fs_mntops }} - persist: no {% endif %} {% endfor %} 07070100000070000081A40000000000000000000000016130D1CF00000270000000000000000000000000000000000000004D00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/post_install.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.mountpoint == '/' %} {{ macros.log('snapper_install', 'snapper_step_four_' ~ device) }} snapper_step_four_{{ device }}: snapper_install.step_four: - root: /mnt {{ macros.log('snapper_install', 'snapper_step_five_' ~ device) }} snapper_step_five_{{ device }}: snapper_install.step_five: - root: /mnt - snapshot_type: single - description: 'after installation' - important: yes - cleanup: number {% endif %} {% endfor %} 07070100000071000081A40000000000000000000000016130D1CF00000217000000000000000000000000000000000000004900000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/software.sls{% import 'macros.yml' as macros %} {% set software = pillar['software'] %} {% set software_config = software.get('config', {}) %} {{ macros.log('pkg', 'install_snapper') }} install_snapper: pkg.installed: - pkgs: - snapper - grub2-snapper-plugin - snapper-zypp-plugin - btrfsprogs {% if software_config.get('minimal') %} - no_recommends: yes {% endif %} {% if not software_config.get('verify') %} - skip_verify: yes {% endif %} - root: /mnt - require: - mount: mount_/mnt 07070100000072000081A40000000000000000000000016130D1CF0000026E000000000000000000000000000000000000004A00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/subvolume.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% for device, info in filesystems.items() %} {% if info.filesystem == 'btrfs' and info.mountpoint == '/' %} {{ macros.log('snapper_install', 'snapper_step_one_' ~ device) }} snapper_step_one_{{ device }}: snapper_install.step_one: - device: {{ device }} - description: 'first root filesystem' {{ macros.log('snapper_install', 'snapper_step_two_' ~ device) }} snapper_step_two_{{ device }}: snapper_install.step_two: - device: {{ device }} - prefix: "{{ info.subvolumes.get('prefix') }}" {% endif %} {% endfor %} 07070100000073000081A40000000000000000000000016130D1CF0000011D000000000000000000000000000000000000004700000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/snapper/umount.sls{% import 'macros.yml' as macros %} {% set filesystems = pillar['filesystems'] %} {% set fs_file = '/mnt'|path_join('.snapshots') %} {{ macros.log('mount', 'umount_' ~ fs_file) }} umount_{{ fs_file }}: mount.unmounted: - name: {{ fs_file }} - requires: mount_{{ fs_file }} 07070100000074000081A40000000000000000000000016130D1CF00000145000000000000000000000000000000000000004100000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/software.sls{% set config = pillar['config'] %} {% if pillar.get('raid') or pillar.get('lvm') or config.get('snapper') %} include: {% if pillar.get('raid') %} - .raid.software {% endif %} {% if pillar.get('lvm') %} - .lvm.software {% endif %} {% if config.get('snapper') %} - .snapper.software {% endif %} {% endif %} 07070100000075000081A40000000000000000000000016130D1CF00000085000000000000000000000000000000000000004300000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/subvolumes.sls{% set config = pillar['config'] %} include: - .btrfs.subvolume {% if config.get('snapper') %} - .snapper.subvolume {% endif %} 07070100000076000081A40000000000000000000000016130D1CF000000A6000000000000000000000000000000000000003F00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/umount.sls{% set config = pillar['config'] %} include: - ..chroot.umount {% if config.get('snapper') %} - .snapper.umount {% endif %} - .btrfs.umount - .device.umount 07070100000077000081A40000000000000000000000016130D1CF00000019000000000000000000000000000000000000004000000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/volumes.slsinclude: - .lvm.volume 07070100000078000081A40000000000000000000000016130D1CF00000121000000000000000000000000000000000000003D00000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/storage/wipe.sls{% import 'macros.yml' as macros %} {% set partitions = salt.partmod.prepare_partition_data(pillar['partitions']) %} {% for device in partitions %} {{ macros.log('module', 'wipe_' ~ device) }} wipe_{{ device }}: module.run: - devices.wipe: - device: {{ device }} {% endfor %}07070100000079000081A40000000000000000000000016130D1CF0000056E000000000000000000000000000000000000003600000000yomi-0.0.1+git.1630589391.4557cfd/salt/yomi/users.sls{% import 'macros.yml' as macros %} {% set users = pillar['users'] %} {% for user in users %} {{ macros.log('module', 'create_user_' ~ user.username) }} create_user_{{ user.username }}: module.run: - user.add: - name: {{ user.username }} - createhome: yes - root: /mnt - unless: grep -q '{{ user.username }}' /mnt/etc/shadow {% if user.get('password') %} {{ macros.log('module', 'set_password_user_' ~ user.username) }} # We should use here the root parameter, but we move to chroot.call # because bsc#1167909 set_password_user_{{ user.username }}: module.run: - chroot.call: - root: /mnt - function: shadow.set_password - name: {{ user.username }} - password: "'{{ user.password }}'" - use_usermod: yes - unless: grep -q '{{ user.username }}:{{ user.password }}' /mnt/etc/shadow {% endif %} {% for certificate in user.get('certificates', []) %} {{ macros.log('module', 'add_certificate_user_' ~ user.username ~ '_' ~ loop.index) }} add_certificate_user_{{ user.username }}_{{ loop.index }}: module.run: - chroot.call: - root: /mnt - function: ssh.set_auth_key - user: {{ user.username }} - key: "'{{ certificate }}'" - unless: grep -q '{{ certificate }}' /mnt/{{ 'home/' if user.username != 'root' else '' }}{{ user.username }}/.ssh/authorized_keys {% endfor %} {% endfor %} 0707010000007A000041ED0000000000000000000000036130D1CF00000000000000000000000000000000000000000000002800000000yomi-0.0.1+git.1630589391.4557cfd/tests0707010000007B000081A40000000000000000000000016130D1CF00000000000000000000000000000000000000000000003400000000yomi-0.0.1+git.1630589391.4557cfd/tests/__init__.py0707010000007C000041ED0000000000000000000000026130D1CF00000000000000000000000000000000000000000000003100000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures0707010000007D000081A40000000000000000000000016130D1CF00000905000000000000000000000000000000000000004000000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/ay_complex.xml<?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns"> <partitioning config:type="list"> <drive> <device>/dev/sda</device> <initialize config:type="boolean">true</initialize> <partitions config:type="list"> <partition> <create config:type="boolean" >false</create> <crypt_fs config:type="boolean">false</crypt_fs> <mount>/</mount> <fstopt> ro,noatime,user,data=ordered,acl,user_xattr </fstopt> <label>mydata</label> <uuid>UUID</uuid> <size>10G</size> <filesystem config:type="symbol">btrfs</filesystem> <mkfs_options>-I 128</mkfs_options> <partition_nr config:type="integer">1</partition_nr> <partition_id config:type="integer">131</partition_id> <partition_type>primary</partition_type> <mountby config:type="symbol">label</mountby> <subvolumes config:type="list"> <path>tmp</path> <path>opt</path> <path>srv</path> <path>var/crash</path> <path>var/lock</path> <path>var/run</path> <path>var/tmp</path> <path>var/spool</path> </subvolumes> <create_subvolumes config:type="boolean" >false</create_subvolumes> <subvolumes_prefix>@</subvolumes_prefix> <lv_name>opt_lv</lv_name> <stripes config:type="integer">2</stripes> <stripesize config:type="integer">4</stripesize> <lvm_group>system</lvm_group> <pool config:type="boolean">false</pool> <used_pool>my_thin_pool</used_pool> <raid_name>/dev/md/0</raid_name> <raid_options> <chunk_size>4</chunk_size> <parity_algorithm>left_asymmetric</parity_algorithm> <raid_type>raid1</raid_type> <device_order config:type="list"> <device>/dev/sdb2</device> <device>/dev/sda1</device> </device_order> </raid_options> <bcache_backing_for>/dev/bcache0</bcache_backing_for> <bcache_caching_for config:type="list"> <listentry>/dev/bcache0</listentry> </bcache_caching_for> <resize config:type="boolean">false</resize> </partition> </partitions> <use>all</use> <type config:type="symbol">CT_DISK</type> <disklabel>gpt</disklabel> <enable_snapshots config:type="boolean">true</enable_snapshots> </drive> </partitioning> </profile> 0707010000007E000081A40000000000000000000000016130D1CF000005B5000000000000000000000000000000000000004100000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/ay_lvm_ext3.xml<?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns"> <partitioning config:type="list"> <drive> <device>/dev/sda</device> <partitions config:type="list"> <partition> <!-- <create config:type="boolean">true</create> --> <lvm_group>system</lvm_group> <partition_type>primary</partition_type> <partition_id config:type="integer">142</partition_id> <partition_nr config:type="integer">1</partition_nr> <size>max</size> </partition> </partitions> <use>all</use> </drive> <drive> <device>/dev/system</device> <is_lvm_vg config:type="boolean">true</is_lvm_vg> <partitions config:type="list"> <partition> <filesystem config:type="symbol">ext3</filesystem> <lv_name>user_lv</lv_name> <mount>/usr</mount> <size>15G</size> </partition> <partition> <filesystem config:type="symbol">ext3</filesystem> <lv_name>opt_lv</lv_name> <mount>/opt</mount> <size>10G</size> </partition> <partition> <filesystem config:type="symbol">ext3</filesystem> <lv_name>var_lv</lv_name> <mount>/var</mount> <size>1G</size> </partition> </partitions> <pesize>4M</pesize> <use>all</use> </drive> </partitioning> </profile> 0707010000007F000081A40000000000000000000000016130D1CF00000565000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/ay_raid_ext3.xml<?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns"> <partitioning config:type="list"> <drive> <device>/dev/sda</device> <partitions config:type="list"> <partition> <filesystem config:type="symbol">ext3</filesystem> <mount>/</mount> <size>20G</size> </partition> <partition> <raid_name>/dev/md/0</raid_name> <size>max</size> </partition> </partitions> <use>all</use> </drive> <drive> <device>/dev/sdb</device> <disklabel>none</disklabel> <partitions config:type="list"> <partition> <raid_name>/dev/md/0</raid_name> </partition> </partitions> <use>all</use> </drive> <drive> <device>/dev/md/0</device> <partitions config:type="list"> <partition> <filesystem config:type="symbol">ext3</filesystem> <mount>/home</mount> <size>40G</size> </partition> <partition> <filesystem config:type="symbol">ext3</filesystem> <mount>/srv</mount> <size>10G</size> </partition> </partitions> <raid_options> <chunk_size>4</chunk_size> <parity_algorithm>left_asymmetric</parity_algorithm> <raid_type>raid1</raid_type> </raid_options> <use>all</use> </drive> </partitioning> </profile> 07070100000080000081A40000000000000000000000016130D1CF00000259000000000000000000000000000000000000004F00000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/ay_raid_no_partition_ext3.xml<?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns"> <partitioning config:type="list"> <drive> <device>/dev/md/0</device> <disklabel>none</disklabel> <partitions config:type="list"> <partition> <mount>/home</mount> <size>40G</size> </partition> </partitions> <raid_options> <chunk_size>4</chunk_size> <parity_algorithm>left_asymmetric</parity_algorithm> <raid_type>raid1</raid_type> </raid_options> <use>all</use> </drive> </partitioning> </profile> 07070100000081000081A40000000000000000000000016130D1CF000006B4000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/ay_single_btrfs.xml<?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns"> <partitioning config:type="list"> <drive> <device>/dev/sda</device> <initialize config:type="boolean">true</initialize> <partitions config:type="list"> <partition> <create config:type="boolean">true</create> <size>1M</size> <format config:type="boolean">false</format> <partition_nr config:type="integer">1</partition_nr> </partition> <partition> <create config:type="boolean">true</create> <mount>swap</mount> <size>2G</size> <format config:type="boolean">true</format> <filesystem config:type="symbol">swap</filesystem> <partition_nr config:type="integer">2</partition_nr> <partition_id config:type="integer">130</partition_id> </partition> <partition> <create config:type="boolean">true</create> <mount>/</mount> <size>max</size> <format config:type="boolean">true</format> <filesystem config:type="symbol">btrfs</filesystem> <partition_nr config:type="integer">3</partition_nr> <partition_id config:type="integer">131</partition_id> <subvolumes config:type="list"> <listentry>tmp</listentry> <listentry>opt</listentry> <listentry>srv</listentry> <listentry> <path>var/lib/pgsql</path> <copy_on_write config:type="boolean">false</copy_on_write> </listentry> </subvolumes> <subvolumes_prefix>@</subvolumes_prefix> </partition> </partitions> <use>all</use> <type>CT_DISK</type> <disklabel>gpt</disklabel> <enable_snapshots config:type="boolean">false</enable_snapshots> </drive> </partitioning> </profile> 07070100000082000081A40000000000000000000000016130D1CF00003902000000000000000000000000000000000000004400000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/ay_single_ext3.xml<?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns"> <general> <mode> <activate_systemd_default_target config:type="boolean"> true </activate_systemd_default_target> <confirm config:type="boolean">true</confirm> <confirm_base_product_license config:type="boolean"> false </confirm_base_product_license> <final_halt config:type="boolean">false</final_halt> <final_reboot config:type="boolean">true</final_reboot> <final_restart_services config:type="boolean"> true </final_restart_services> <forceboot config:type="boolean">false</forceboot> <halt config:type="boolean">false</halt> <max_systemd_wait config:type="integer">30</max_systemd_wait> <ntp_sync_time_before_installation> 0.de.pool.ntp.org </ntp_sync_time_before_installation> <second_stage config:type="boolean">true</second_stage> </mode> <proposals config:type="list"> <proposal>partitions_proposal</proposal> <proposal>timezone_proposal</proposal> <proposal>software_proposal</proposal> </proposals> <self_update config:type="boolean">true</self_update> <self_update_url> http://example.com/updates/$arch </self_update_url> <semi-automatic config:type="list"> <semi-automatic_entry>networking</semi-automatic_entry> <semi-automatic_entry>scc</semi-automatic_entry> <semi-automatic_entry>partitioning</semi-automatic_entry> </semi-automatic> <signature-handling> <accept_unsigned_file config:type="boolean"> false </accept_unsigned_file> <accept_file_without_checksum config:type="boolean"> false </accept_file_without_checksum> <accept_verification_failed config:type="boolean"> false </accept_verification_failed> <accept_unknown_gpg_key config:type="boolean"> false </accept_unknown_gpg_key> <accept_non_trusted_gpg_key config:type="boolean"> false </accept_non_trusted_gpg_key> <import_gpg_key config:type="boolean"> false </import_gpg_key> </signature-handling> <storage> <start_multipath config:type="boolean">false</start_multipath> </storage> <wait> <pre-modules config:type="list"> <module> <name>networking</name> <sleep> <time config:type="integer">10</time> <feedback config:type="boolean">true</feedback> </sleep> <script> <source>echo foo</source> <debug config:type="boolean">false</debug> </script> </module> </pre-modules> <post-modules config:type="list"> <module> <name>networking</name> <sleep> <time config:type="integer">10</time> <feedback config:type="boolean">true</feedback> </sleep> <script> <source>echo foo</source> <debug config:type="boolean">false</debug> </script> </module> </post-modules> </wait> <cio_ignore config:type="boolean">false</cio_ignore> </general> <report> <errors> <show config:type="boolean">true</show> <timeout config:type="integer">0</timeout> <log config:type="boolean">true</log> </errors> <warnings> <show config:type="boolean">true</show> <timeout config:type="integer">10</timeout> <log config:type="boolean">true</log> </warnings> <messages> <show config:type="boolean">true</show> <timeout config:type="integer">10</timeout> <log config:type="boolean">true</log> </messages> <yesno_messages> <show config:type="boolean">true</show> <timeout config:type="integer">10</timeout> <log config:type="boolean">true</log> </yesno_messages> </report> <suse_register> <do_registration config:type="boolean">true</do_registration> <email>tux@example.com</email> <reg_code>MY_SECRET_REGCODE</reg_code> <install_updates config:type="boolean">true</install_updates> <slp_discovery config:type="boolean">false</slp_discovery> <reg_server> https://smt.example.com </reg_server> <reg_server_cert_fingerprint_type> SHA1 </reg_server_cert_fingerprint_type> <reg_server_cert_fingerprint> 01:AB...:EF </reg_server_cert_fingerprint> <reg_server_cert> http://smt.example.com/smt.crt </reg_server_cert> <addons config:type="list"> <addon> <name>sle-module-basesystem</name> <version>15.1</version> <arch>x86_64</arch> </addon> </addons> </suse_register> <bootloader> <loader_type> grub2-efi </loader_type> <global> <activate config:type="boolean">true</activate> <append>nomodeset vga=0x317</append> <boot_boot>false</boot_boot> <boot_custom>/dev/sda</boot_custom> <boot_extended>false</boot_extended> <boot_mbr>false</boot_mbr> <boot_root>false</boot_root> <generic_mbr config:type="boolean">false</generic_mbr> <gfxmode>1280x1024x24</gfxmode> <os_prober config:type="boolean">false</os_prober> <cpu_mitigations>auto</cpu_mitigations> <suse_btrfs config:type="boolean">true</suse_btrfs> <serial> serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1 </serial> <terminal>serial</terminal> <timeout config:type="integer">10</timeout> <trusted_boot config:type="boolean">true</trusted_boot> <vgamode>0x317</vgamode> <xen_append>nomodeset vga=0x317</xen_append> <xen_kernel_append>dom0_mem=768M</xen_kernel_append> </global> <device_map config:type="list"> <device_map_entry> <firmware>hd0</firmware> <linux>/dev/disk/by-id/ata-ST3500418AS_6VM23FX0</linux> </device_map_entry> </device_map> </bootloader> <partitioning config:type="list"> <drive> <device>/dev/sda</device> <initialize config:type="boolean">true</initialize> <partitions config:type="list"> <partition> <create config:type="boolean">true</create> <size>1M</size> <format config:type="boolean">false</format> <partition_nr config:type="integer">1</partition_nr> </partition> <partition> <create config:type="boolean">true</create> <mount>swap</mount> <size>2G</size> <format config:type="boolean">true</format> <filesystem config:type="symbol">swap</filesystem> <partition_nr config:type="integer">2</partition_nr> <partition_id config:type="integer">130</partition_id> </partition> <partition> <create config:type="boolean">true</create> <mount>/</mount> <size>max</size> <format config:type="boolean">true</format> <filesystem config:type="symbol">ext3</filesystem> <partition_nr config:type="integer">3</partition_nr> <partition_id config:type="integer">131</partition_id> </partition> </partitions> <use>all</use> <type>CT_DISK</type> <disklabel>gpt</disklabel> <enable_snapshots config:type="boolean">false</enable_snapshots> </drive> </partitioning> <language> <language>en_GB</language> <languages>de_DE,en_US</languages> </language> <timezone> <hwclock>UTC</hwclock> <timezone>Europe/Berlin</timezone> </timezone> <keyboard> <keymap>german</keymap> </keyboard> <software> <products config:type="list"> <product>SLED</product> </products> <patterns config:type="list"> <pattern>directory_server</pattern> </patterns> <packages config:type="list"> <package>apache</package> <package>postfix</package> </packages> <remove-packages config:type="list"> <package>postfix</package> </remove-packages> <do_online_update config:type="boolean">true</do_online_update> <kernel>kernel-default</kernel> <install_recommended config:type="boolean">false</install_recommended> <post-packages config:type="list"> <package>yast2-cim</package> </post-packages> <post-patterns config:type="list"> <pattern>apparmor</pattern> </post-patterns> </software> <add-on> <add_on_products config:type="list"> <listentry> <media_url>cd:///sdk</media_url> <product>sle-sdk</product> <alias>SLES SDK</alias> <product_dir>/</product_dir> <priority config:type="integer">20</priority> <ask_on_error config:type="boolean">false</ask_on_error> <confirm_license config:type="boolean">false</confirm_license> <name>SUSE Linux Enterprise Software Development Kit</name> </listentry> </add_on_products> <add_on_others config:type="list"> <listentry> <media_url>https://download.opensuse.org/repositories/YaST:/Head/openSUSE_Leap_15.1/</media_url> <alias>yast2_head</alias> <priority config:type="integer">30</priority> <name>Latest YaST2 packages from OBS</name> </listentry> </add_on_others> </add-on> <services-manager> <default_target>multi-user</default_target> <services> <disable config:type="list"> <service>libvirtd</service> </disable> <enable config:type="list"> <service>sshd</service> </enable> <on_demand config:type="list"> <service>cups</service> </on_demand> </services> </services-manager> <networking> <dns> <dhcp_hostname config:type="boolean">true</dhcp_hostname> <domain>site</domain> <hostname>linux-bqua</hostname> <nameservers config:type="list"> <nameserver>192.168.1.116</nameserver> <nameserver>192.168.1.117</nameserver> <nameserver>192.168.1.118</nameserver> </nameservers> <resolv_conf_policy>auto</resolv_conf_policy> <searchlist config:type="list"> <search>example.com</search> <search>example.net</search> </searchlist> <write_hostname config:type="boolean">false</write_hostname> </dns> <interfaces config:type="list"> <interface> <bootproto>dhcp</bootproto> <device>eth0</device> <startmode>auto</startmode> </interface> <interface> <bootproto>static</bootproto> <broadcast>127.255.255.255</broadcast> <device>lo</device> <firewall>no</firewall> <ipaddr>127.0.0.1</ipaddr> <netmask>255.0.0.0</netmask> <network>127.0.0.0</network> <prefixlen>8</prefixlen> <startmode>nfsroot</startmode> <usercontrol>no</usercontrol> </interface> </interfaces> <ipv6 config:type="boolean">true</ipv6> <keep_install_network config:type="boolean">false</keep_install_network> <managed config:type="boolean">false</managed> ###### NetworkManager? <net-udev config:type="list"> <rule> <name>eth0</name> <rule>ATTR{address}</rule> <value>00:30:6E:08:EC:80</value> </rule> </net-udev> <s390-devices config:type="list"> <listentry> <chanids>0.0.0800 0.0.0801 0.0.0802</chanids> <type>qeth</type> </listentry> </s390-devices> <routing> <ipv4_forward config:type="boolean">false</ipv4_forward> <ipv6_forward config:type="boolean">false</ipv6_forward> <routes config:type="list"> <route> <destination>192.168.2.1</destination> <device>eth0</device> <extrapara>foo</extrapara> <gateway>-</gateway> <netmask>-</netmask> </route> <route> <destination>default</destination> <device>eth0</device> <gateway>192.168.1.1</gateway> <netmask>-</netmask> </route> <route> <destination>default</destination> <device>lo</device> <gateway>192.168.5.1</gateway> <netmask>-</netmask> </route> </routes> </routing> </networking> <users config:type="list"> <user> <username>root</username> <user_password>password</user_password> <uid>1001</uid> <gid>100</gid> <encrypted config:type="boolean">false</encrypted> <fullname>Root User</fullname> <authorized_keys config:type="list"> <listentry>command="/opt/login.sh" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKLt1vnW2vTJpBp3VK91rFsBvpY97NljsVLdgUrlPbZ/L51FerQQ+djQ/ivDASQjO+567nMGqfYGFA/De1EGMMEoeShza67qjNi14L1HBGgVojaNajMR/NI2d1kDyvsgRy7D7FT5UGGUNT0dlcSD3b85zwgHeYLidgcGIoKeRi7HpVDOOTyhwUv4sq3ubrPCWARgPeOLdVFa9clC8PTZdxSeKp4jpNjIHEyREPin2Un1luCIPWrOYyym7aRJEPopCEqBA9HvfwpbuwBI5F0uIWZgSQLfpwW86599fBo/PvMDa96DpxH1VlzJlAIHQsMkMHbsCazPNC0++Kp5ZVERiH root@example.net</listentry> </authorized_keys> </user> <user> <username>tux</username> <user_password>password</user_password> <uid>1002</uid> <gid>100</gid> <encrypted config:type="boolean">false</encrypted> <fullname>Plain User</fullname> <home>/Users/plain</home> <password_settings> <max>120</max> <inact>5</inact> </password_settings> </user> </users> <groups config:type="list"> <group> <gid>100</gid> <groupname>users</groupname> <userlist>bob,alice</userlist> </group> </groups> <login_settings> <autologin_user>vagrant</autologin_user> <password_less_login config:type="boolean">true</password_less_login> </login_settings> <sysconfig config:type="list" > <sysconfig_entry> <sysconfig_key>XNTPD_INITIAL_NTPDATE</sysconfig_key> <sysconfig_path>/etc/sysconfig/xntp</sysconfig_path> <sysconfig_value>ntp.host.com</sysconfig_value> </sysconfig_entry> <sysconfig_entry> <sysconfig_key>HTTP_PROXY</sysconfig_key> <sysconfig_path>/etc/sysconfig/proxy</sysconfig_path> <sysconfig_value>proxy.host.com:3128</sysconfig_value> </sysconfig_entry> <sysconfig_entry> <sysconfig_key>FTP_PROXY</sysconfig_key> <sysconfig_path>/etc/sysconfig/proxy</sysconfig_path> <sysconfig_value>proxy.host.com:3128</sysconfig_value> </sysconfig_entry> </sysconfig> <firewall> <enable_firewall>true</enable_firewall> <log_denied_packets>all</log_denied_packets> <default_zone>external</default_zone> <zones config:type="list"> <zone> <name>public</name> <interfaces config:type="list"> <interface>eth0</interface> </interfaces> <services config:type="list"> <service>ssh</service> <service>dhcp</service> <service>dhcpv6</service> <service>samba</service> <service>vnc-server</service> </services> <ports config:type="list"> <port>21/udp</port> <port>22/udp</port> <port>80/tcp</port> <port>443/tcp</port> <port>8080/tcp</port> </ports> </zone> <zone> <name>dmz</name> <interfaces config:type="list"> <interface>eth1</interface> </interfaces> </zone> </zones> </firewall> </profile> 07070100000083000081A40000000000000000000000016130D1CF00000983000000000000000000000000000000000000004500000000yomi-0.0.1+git.1630589391.4557cfd/tests/fixtures/list_extensions.txt[1mAVAILABLE EXTENSIONS AND MODULES[0m [1mBasesystem Module 15 SP2 x86_64[0m [33m(Activated)[0m Deactivate with: SUSEConnect [31m-d[0m -p sle-module-basesystem/15.2/x86_64 [1mContainers Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-containers/15.2/x86_64 [1mDesktop Applications Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-desktop-applications/15.2/x86_64 [1mDevelopment Tools Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-development-tools/15.2/x86_64 [1mSUSE Linux Enterprise Workstation Extension 15 SP2 x86_64 (ALPHA)[0m Activate with: SUSEConnect -p sle-we/15.2/x86_64 -r [32m[1mADDITIONAL REGCODE[0m [1mPython 2 Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-python2/15.2/x86_64 [1mSUSE Linux Enterprise Live Patching 15 SP2 x86_64 (ALPHA)[0m Activate with: SUSEConnect -p sle-module-live-patching/15.2/x86_64 -r [32m[1mADDITIONAL REGCODE[0m [1mSUSE Package Hub 15 SP2 x86_64[0m Activate with: SUSEConnect -p PackageHub/15.2/x86_64 [1mServer Applications Module 15 SP2 x86_64[0m [33m(Activated)[0m Deactivate with: SUSEConnect [31m-d[0m -p sle-module-server-applications/15.2/x86_64 [1mLegacy Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-legacy/15.2/x86_64 [1mPublic Cloud Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-public-cloud/15.2/x86_64 [1mSUSE Linux Enterprise High Availability Extension 15 SP2 x86_64 (ALPHA)[0m Activate with: SUSEConnect -p sle-ha/15.2/x86_64 -r [32m[1mADDITIONAL REGCODE[0m [1mWeb and Scripting Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-web-scripting/15.2/x86_64 [1mTransactional Server Module 15 SP2 x86_64[0m Activate with: SUSEConnect -p sle-module-transactional-server/15.2/x86_64 [1mREMARKS[0m [31m(Not available)[0m The module/extension is [1mnot[0m enabled on your RMT/SMT [33m(Activated)[0m The module/extension is activated on your system [1mMORE INFORMATION[0m You can find more information about available modules here: https://www.suse.com/documentation/sles-15/singlehtml/art_modules/art_modules.html 07070100000084000081ED0000000000000000000000016130D1CF000006FB000000000000000000000000000000000000003500000000yomi-0.0.1+git.1630589391.4557cfd/tests/run_tests.sh#! /bin/bash # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Setup the environment so the Python modules living in salt/_* can be # found. cd "$(dirname "${BASH_SOURCE[0]}")" test_env=$(mktemp -d -t tmp.XXXX) tear_down() { rm -fr "$test_env" } trap tear_down EXIT # Create the temporary Python modules, that once added in the # PYTHON_PATH can be found and imported touch "$test_env"/__init__.py for module in modules states grains utils; do mkdir "$test_env"/"$module" touch "$test_env"/"$module"/__init__.py [ "$(ls -A ../salt/_"$module")" ] && ln -sr ../salt/_"$module"/* "$test_env"/"$module"/ done for binary in autoyast2yomi monitor; do ln -sr ../"$binary" "$test_env"/"$binary".py done if [ -z $PYTHONPATH ]; then export PYTHONPATH="$test_env":"$test_env"/utils:. else export PYTHONPATH="$PATHONPATH":"$test_env":"$test_env"/utils:. fi if [ -z "$*" ]; then python3 -m unittest discover else python3 -m unittest "$@" fi 07070100000085000081A40000000000000000000000016130D1CF00005A1B000000000000000000000000000000000000003E00000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_autoyast2yomi.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import os.path import unittest from unittest.mock import patch import xml.etree.ElementTree as ET import autoyast2yomi class AutoYaST2YomiTestCase(unittest.TestCase): def _parse_xml(self, name): name = os.path.join(os.path.dirname(__file__), "fixtures/{}".format(name)) return ET.parse(name) def setUp(self): self.maxDiff = None def test__find(self): control = self._parse_xml("ay_single_ext3.xml") general = autoyast2yomi.Convert._find(control.getroot(), "general") self.assertEqual(general.tag, "{http://www.suse.com/1.0/yast2ns}general") non_existent = autoyast2yomi.Convert._find(control, "non-existent") self.assertIsNone(non_existent) def test__get_tag(self): control = ET.fromstring('<a xmlns="http://www.suse.com/1.0/yast2ns"><b/></a>') self.assertEqual(autoyast2yomi.Convert._get_tag(control[0]), "b") def test__get_type(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns" ' 'xmlns:config="http://www.suse.com/1.0/configns">' '<b config:type="integer"/></a>' ) self.assertEqual(autoyast2yomi.Convert._get_type(control[0]), "integer") control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns" ' 'xmlns:config="http://www.suse.com/1.0/configns">' "<b/></a>" ) self.assertIsNone(autoyast2yomi.Convert._get_type(control[0])) def test__get_text(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns"><b>text</b></a>' ) value = autoyast2yomi.Convert._get_text(control[0]) self.assertEqual(value, "text") non_text = autoyast2yomi.Convert._get_text(None) self.assertIsNone(non_text) def test__get_bool(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns"><b>true</b>' "<c>false</c></a>" ) value = autoyast2yomi.Convert._get_bool(control[0]) self.assertTrue(value) value = autoyast2yomi.Convert._get_bool(control[1]) self.assertFalse(value) non_bool = autoyast2yomi.Convert._get_bool(control) self.assertIsNone(non_bool) def test__get_int(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns"><b>0</b>' "<c>1</c></a>" ) value = autoyast2yomi.Convert._get_int(control[0]) self.assertEqual(value, 0) value = autoyast2yomi.Convert._get_int(control[1]) self.assertEqual(value, 1) non_int = autoyast2yomi.Convert._get_int(control) self.assertIsNone(non_int) def test__parse_single_text(self): control = ET.fromstring('<a xmlns="http://www.suse.com/1.0/yast2ns">text</a>') self.assertEqual(autoyast2yomi.Convert._parse(control), {"a": "text"}) def test__parse_single_bool(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns" ' 'xmlns:config="http://www.suse.com/1.0/configns" ' 'config:type="boolean">true</a>' ) self.assertEqual(autoyast2yomi.Convert._parse(control), {"a": True}) def test__parse_single_int(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns" ' 'xmlns:config="http://www.suse.com/1.0/configns" ' 'config:type="integer">10</a>' ) self.assertEqual(autoyast2yomi.Convert._parse(control), {"a": 10}) def test__parse_single_list(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns" ' 'xmlns:config="http://www.suse.com/1.0/configns" ' 'config:type="list"><b>one</b><b>two</b></a>' ) self.assertEqual(autoyast2yomi.Convert._parse(control), {"a": ["one", "two"]}) def test__parse_single_dict(self): control = ET.fromstring( '<a xmlns="http://www.suse.com/1.0/yast2ns">' "<b>text</b><c>other</c></a>" ) self.assertEqual( autoyast2yomi.Convert._parse(control), {"a": {"b": "text", "c": "other"}} ) def test__parse_complex(self): control = self._parse_xml("ay_complex.xml").getroot() self.assertEqual( autoyast2yomi.Convert._parse(control), { "profile": { "partitioning": [ { "device": "/dev/sda", "disklabel": "gpt", "enable_snapshots": True, "initialize": True, "partitions": [ { "bcache_backing_for": "/dev/bcache0", "bcache_caching_for": ["/dev/bcache0"], "create": False, "create_subvolumes": False, "crypt_fs": False, "filesystem": "btrfs", "fstopt": ( "ro,noatime,user,data=ordered," "acl,user_xattr" ), "label": "mydata", "lv_name": "opt_lv", "lvm_group": "system", "mkfs_options": "-I 128", "mount": "/", "mountby": "label", "partition_id": 131, "partition_nr": 1, "partition_type": "primary", "pool": False, "raid_name": "/dev/md/0", "raid_options": { "chunk_size": "4", "device_order": ["/dev/sdb2", "/dev/sda1"], "parity_algorithm": "left_asymmetric", "raid_type": "raid1", }, "resize": False, "size": "10G", "stripes": 2, "stripesize": 4, "subvolumes": [ "tmp", "opt", "srv", "var/crash", "var/lock", "var/run", "var/tmp", "var/spool", ], "subvolumes_prefix": "@", "used_pool": "my_thin_pool", "uuid": "UUID", } ], "type": "CT_DISK", "use": "all", } ] } }, ) @patch("autoyast2yomi.logging") def test__convert_config_single_ext3(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_config() self.assertEqual( convert.pillar, { "config": { "events": True, "reboot": True, "snapper": False, "locale": "en_GB", "keymap": "de-nodeadkeys", "timezone": "Europe/Berlin", "hostname": "linux-bqua", "target": "multi-user", } }, ) @patch("autoyast2yomi.logging") def test__convert_partitions_single_ext3(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_partitions() self.assertEqual( convert.pillar, { "partitions": { "devices": { "/dev/sda": { "label": "gpt", "partitions": [ {"number": 1, "size": "1M", "type": "boot"}, {"number": 2, "size": "2G", "type": "swap"}, {"number": 3, "size": "rest", "type": "linux"}, ], } } } }, ) @patch("autoyast2yomi.logging") def test__convert_partitions_lvm_ext3(self, logging): control = self._parse_xml("ay_lvm_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_partitions() self.assertEqual( convert.pillar, { "partitions": { "devices": { "/dev/sda": { "label": "gpt", "partitions": [ {"number": 1, "size": "rest", "type": "lvm"} ], } } } }, ) @patch("autoyast2yomi.logging") def test__convert_partitions_raid_ext3(self, logging): control = self._parse_xml("ay_raid_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_partitions() self.assertEqual( convert.pillar, { "partitions": { "devices": { "/dev/sda": { "label": "gpt", "partitions": [ {"number": 1, "size": "20G", "type": "linux"}, {"number": 2, "size": "rest", "type": "raid"}, ], }, "/dev/sdb": { "partitions": [ {"number": 1, "size": "rest", "type": "raid"}, ] }, "/dev/md/0": { "partitions": [ {"number": 1, "size": "40G", "type": "linux"}, {"number": 2, "size": "10G", "type": "linux"}, ] }, } } }, ) @patch("autoyast2yomi.logging") def test__convert_lvm_ext3(self, logging): control = self._parse_xml("ay_lvm_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_lvm() self.assertEqual( convert.pillar, { "lvm": { "system": { "devices": ["/dev/sda1"], "physicalextentsize": "4M", "volumes": [ {"name": "user_lv", "size": "15G"}, {"name": "opt_lv", "size": "10G"}, {"name": "var_lv", "size": "1G"}, ], } } }, ) @patch("autoyast2yomi.logging") def test__convert_raid_ext3(self, logging): control = self._parse_xml("ay_raid_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_raid() self.assertEqual( convert.pillar, { "raid": { "/dev/md/0": { "level": "raid1", "devices": ["/dev/sda2", "/dev/sdb1"], "chunk": "4", "parity": "left-asymmetric", } } }, ) @patch("autoyast2yomi.logging") def test__convert_filesystems_single_ext3(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_filesystems() self.assertEqual( convert.pillar, { "filesystems": { "/dev/sda2": {"filesystem": "swap", "mountpoint": "swap"}, "/dev/sda3": {"filesystem": "ext3", "mountpoint": "/"}, } }, ) @patch("autoyast2yomi.logging") def test__convert_filesystems_single_btrfs(self, logging): control = self._parse_xml("ay_single_btrfs.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_filesystems() self.assertEqual( convert.pillar, { "filesystems": { "/dev/sda2": {"filesystem": "swap", "mountpoint": "swap"}, "/dev/sda3": { "filesystem": "btrfs", "mountpoint": "/", "subvolumes": { "prefix": "@", "subvolume": [ {"path": "tmp"}, {"path": "opt"}, {"path": "srv"}, {"path": "var/lib/pgsql", "copy_on_write": False}, ], }, }, } }, ) @patch("autoyast2yomi.logging") def test__convert_filesystems_lvm_ext3(self, logging): control = self._parse_xml("ay_lvm_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_filesystems() self.assertEqual( convert.pillar, { "filesystems": { "/dev/system/user_lv": { "filesystem": "ext3", "mountpoint": "/usr", }, "/dev/system/opt_lv": {"filesystem": "ext3", "mountpoint": "/opt"}, "/dev/system/var_lv": {"filesystem": "ext3", "mountpoint": "/var"}, } }, ) @patch("autoyast2yomi.logging") def test__convert_filesystems_raid_ext3(self, logging): control = self._parse_xml("ay_raid_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_filesystems() self.assertEqual( convert.pillar, { "filesystems": { "/dev/sda1": {"filesystem": "ext3", "mountpoint": "/"}, "/dev/md/0p1": {"filesystem": "ext3", "mountpoint": "/home"}, "/dev/md/0p2": {"filesystem": "ext3", "mountpoint": "/srv"}, } }, ) @patch("autoyast2yomi.logging") def test__convert_bootloader(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_bootloader() self.assertEqual( convert.pillar, { "bootloader": { "device": "/dev/sda", "timeout": 10, "kernel": ( "splash=silent quiet nomodeset vga=0x317 " "noibrs noibpb nopti nospectre_v2 nospectre_v1 " "l1tf=off nospec_store_bypass_disable " "no_stf_barrier mds=off mitigations=off" ), "terminal": "serial", "serial_command": ( "serial --speed=115200 --unit=0 " "--word=8 --parity=no --stop=1" ), "gfxmode": "1280x1024x24", "theme": True, "disable_os_prober": True, } }, ) @patch("autoyast2yomi.logging") def test__convert_software(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_software() self.assertEqual( convert.pillar, { "software": { "config": {"minimal": True}, "repositories": { "SLES SDK": "cd:///sdk", "yast2_head": ( "https://download.opensuse.org/repositories" "/YaST:/Head/openSUSE_Leap_15.1/" ), }, "packages": [ "product:SLED", "pattern:directory_server", "apache", "postfix", "kernel-default", ], } }, ) @patch("autoyast2yomi.logging") def test__convert_suseconnect(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_suseconnect() self.assertEqual( convert.pillar, { "suseconnect": { "config": { "regcode": "MY_SECRET_REGCODE", "email": "tux@example.com", "url": "https://smt.example.com", }, "products": ["sle-module-basesystem/15.1/x86_64"], "packages": ["pattern:apparmor", "yast2-cim"], }, }, ) @patch("autoyast2yomi.logging") def test__convert_salt_minion(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_salt_minion() self.assertEqual(convert.pillar, {"salt-minion": {"configure": True}}) @patch("autoyast2yomi.logging") def test__convert_services(self, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) convert._convert_services() self.assertEqual( convert.pillar, { "services": { "enabled": ["sshd.service", "cups.socket"], "disabled": ["libvirtd.service", "cups.service"], } }, ) def test__password(self): self.assertEqual(autoyast2yomi.Convert._password({}), None) self.assertEqual( autoyast2yomi.Convert._password( {"user_password": "linux"}, salt="$1$wYJUgpM5" ), "$1$wYJUgpM5$RXMMeASDc035eX.NbYWFl0", ) self.assertEqual( autoyast2yomi.Convert._password( { "user_password": "$1$wYJUgpM5$RXMMeASDc035eX.NbYWFl0", "encrypted": True, } ), "$1$wYJUgpM5$RXMMeASDc035eX.NbYWFl0", ) @patch("autoyast2yomi.logging") @patch("autoyast2yomi.Convert._password") def test__convert_users(self, _password, logging): control = self._parse_xml("ay_single_ext3.xml") convert = autoyast2yomi.Convert(control) convert._control = autoyast2yomi.Convert._parse(control.getroot()) _password.return_value = "<hash>" convert._convert_users() self.assertEqual( convert.pillar, { "users": [ { "username": "root", "password": "<hash>", "certificates": [ ( "AAAAB3NzaC1yc2EAAAADAQABAAABAQDKLt1vnW2vTJpBp3VK91" "rFsBvpY97NljsVLdgUrlPbZ/L51FerQQ+djQ/ivDASQjO+567n" "MGqfYGFA/De1EGMMEoeShza67qjNi14L1HBGgVojaNajMR/NI2" "d1kDyvsgRy7D7FT5UGGUNT0dlcSD3b85zwgHeYLidgcGIoKeRi" "7HpVDOOTyhwUv4sq3ubrPCWARgPeOLdVFa9clC8PTZdxSeKp4j" "pNjIHEyREPin2Un1luCIPWrOYyym7aRJEPopCEqBA9Hvfwpbuw" "BI5F0uIWZgSQLfpwW86599fBo/PvMDa96DpxH1VlzJlAIHQsMk" "MHbsCazPNC0++Kp5ZVERiH" ) ], }, {"username": "tux", "password": "<hash>"}, ] }, ) 07070100000086000081A40000000000000000000000016130D1CF000101DE000000000000000000000000000000000000003800000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_devices.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import textwrap import unittest from unittest.mock import patch from modules import devices class DevicesTestCase(unittest.TestCase): def test_udev(self): self.assertEqual(devices._udev({"A": {"B": 1}}, "a.b"), 1) self.assertEqual(devices._udev({"A": {"B": 1}}, "A.B"), 1) self.assertEqual(devices._udev({"A": {"B": 1}}, "a.c"), "n/a") self.assertEqual(devices._udev({"A": [1, 2]}, "a.b"), "n/a") self.assertEqual(devices._udev({"A": {"B": 1}}, ""), {"A": {"B": 1}}) def test_match(self): self.assertTrue(devices._match({"A": {"B": 1}}, {"a.b": 1})) self.assertFalse(devices._match({"A": {"B": 1}}, {"a.b": 2})) self.assertTrue(devices._match({"A": {"B": 1}}, {"a.b": [1, 2]})) self.assertFalse(devices._match({"A": {"B": 1}}, {"a.b": [2, 3]})) self.assertTrue(devices._match({"A": {"B": [1, 2]}}, {"a.b": 1})) self.assertTrue(devices._match({"A": {"B": [1, 2]}}, {"a.b": [1, 3]})) self.assertFalse(devices._match({"A": {"B": [1, 2]}}, {"a.b": [3, 4]})) self.assertTrue(devices._match({"A": 1}, {})) @patch("modules.devices.__grains__") @patch("modules.devices.__salt__") def test_devices(self, __salt__, __grains__): cdrom = { "S": ["dvd", "cdrom"], "E": {"ID_BUS": "ata"}, } usb = { "E": {"ID_BUS": "usb"}, } hd = { "E": {"ID_BUS": "ata"}, } __grains__.__getitem__.return_value = ["sda", "sdb", "sr0"] __salt__.__getitem__.return_value = lambda d: { "sda": hd, "sdb": usb, "sr0": cdrom, }[d] self.assertEqual(devices.filter_({"e.id_bus": "ata"}, {}), ["sda", "sr0"]) self.assertEqual(devices.filter_({"e.id_bus": "usb"}, {}), ["sdb"]) self.assertEqual( devices.filter_({"e.id_bus": "ata"}, {"s": ["cdrom"]}), ["sda"] ) def test__hwinfo_parse_short(self): hwinfo = textwrap.dedent( """ cpu: QEMU Virtual CPU version 2.5+, 3591 MHz keyboard: /dev/input/event0 AT Translated Set 2 keyboard mouse: /dev/input/mice VirtualPS/2 VMware VMMouse /dev/input/mice VirtualPS/2 VMware VMMouse graphics card: VGA compatible controller storage: Floppy disk controller Red Hat Qemu virtual machine network: ens3 Virtio Ethernet Card 0 network interface: lo Loopback network interface ens3 Ethernet network interface disk: /dev/fd0 Disk /dev/sda QEMU HARDDISK cdrom: /dev/sr0 QEMU DVD-ROM floppy: /dev/fd0 Floppy Disk bios: BIOS bridge: Red Hat Qemu virtual machine Red Hat Qemu virtual machine Red Hat Qemu virtual machine memory: Main Memory unknown: FPU DMA controller PIC Keyboard controller /dev/lp0 Parallel controller PS/2 Controller Red Hat Virtio network device /dev/ttyS0 16550A """ ) self.assertEqual( devices._hwinfo_parse_short(hwinfo), { "cpu": {0: "QEMU Virtual CPU version 2.5+, 3591 MHz"}, "keyboard": {"/dev/input/event0": "AT Translated Set 2 keyboard"}, "mouse": {"/dev/input/mice": "VirtualPS/2 VMware VMMouse"}, "graphics card": {0: "VGA compatible controller"}, "storage": { 0: "Floppy disk controller", 1: "Red Hat Qemu virtual machine", }, "network": {"ens3": "Virtio Ethernet Card 0"}, "network interface": { "lo": "Loopback network interface", "ens3": "Ethernet network interface", }, "disk": {"/dev/fd0": "Disk", "/dev/sda": "QEMU HARDDISK"}, "cdrom": {"/dev/sr0": "QEMU DVD-ROM"}, "floppy": {"/dev/fd0": "Floppy Disk"}, "bios": {0: "BIOS"}, "bridge": { 0: "Red Hat Qemu virtual machine", 1: "Red Hat Qemu virtual machine", 2: "Red Hat Qemu virtual machine", }, "memory": {0: "Main Memory"}, "unknown": { 0: "FPU", 1: "DMA controller", 2: "PIC", 3: "Keyboard controller", "/dev/lp0": "Parallel controller", 4: "PS/2 Controller", 5: "Red Hat Virtio network device", "/dev/ttyS0": "16550A", }, }, ) def test__hwinfo_parse_full_floppy(self): hwinfo = textwrap.dedent( """ 01: None 00.0: 0102 Floppy disk controller [Created at floppy.112] Unique ID: rdCR.3wRL2_g4d2B Hardware Class: storage Model: "Floppy disk controller" I/O Port: 0x3f2 (rw) I/O Ports: 0x3f4-0x3f5 (rw) I/O Port: 0x3f7 (rw) DMA: 2 Config Status: cfg=new, avail=yes, need=no, active=unknown 02: Floppy 00.0: 10603 Floppy Disk [Created at floppy.127] Unique ID: sPPV.oZ89vuho4Y3 Parent ID: rdCR.3wRL2_g4d2B Hardware Class: floppy Model: "Floppy Disk" Device File: /dev/fd0 Size: 3.5 '' Config Status: cfg=new, avail=yes, need=no, active=unknown Size: 5760 sectors a 512 bytes Capacity: 0 GB (2949120 bytes) Drive status: no medium Config Status: cfg=new, avail=yes, need=no, active=unknown Attached to: #1 (Floppy disk controller) """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "01": { "None 00.0": "0102 Floppy disk controller", "Note": "Created at floppy.112", "Unique ID": "rdCR.3wRL2_g4d2B", "Hardware Class": "storage", "Model": "Floppy disk controller", "I/O Ports": ["0x3f2 (rw)", "0x3f4-0x3f5 (rw)", "0x3f7 (rw)"], "DMA": "2", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "02": { "Floppy 00.0": "10603 Floppy Disk", "Note": "Created at floppy.127", "Unique ID": "sPPV.oZ89vuho4Y3", "Parent ID": "rdCR.3wRL2_g4d2B", "Hardware Class": "floppy", "Model": "Floppy Disk", "Device File": "/dev/fd0", "Size": ["3.5 ''", "5760 sectors a 512 bytes"], "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, "Capacity": "0 GB (2949120 bytes)", "Drive status": "no medium", "Attached to": {"Handle": "#1 (Floppy disk controller)"}, }, }, ) def test__hwinfo_parse_full_bios(self): hwinfo = textwrap.dedent( """ 03: None 00.0: 10105 BIOS [Created at bios.186] Unique ID: rdCR.lZF+r4EgHp4 Hardware Class: bios BIOS Keyboard LED Status: Scroll Lock: off Num Lock: off Caps Lock: off Serial Port 0: 0x3f8 Parallel Port 0: 0x378 Base Memory: 639 kB PnP BIOS: @@@0000 MP spec rev 1.4 info: OEM id: "BOCHSCPU" Product id: "0.1" 1 CPUs (0 disabled) BIOS32 Service Directory Entry: 0xfd2b0 SMBIOS Version: 2.8 BIOS Info: #0 Vendor: "SeaBIOS" Version: "rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org" Date: "04/01/2014" Start Address: 0xe8000 ROM Size: 64 kB Features: 0x04000000000000000008 System Info: #256 Manufacturer: "QEMU" Product: "Standard PC (i440FX + PIIX, 1996)" Version: "pc-i440fx-4.0" UUID: undefined Wake-up: 0x06 (Power Switch) Chassis Info: #768 Manufacturer: "QEMU" Version: "pc-i440fx-4.0" Type: 0x01 (Other) Bootup State: 0x03 (Safe) Power Supply State: 0x03 (Safe) Thermal State: 0x03 (Safe) Security Status: 0x02 (Unknown) Processor Info: #1024 Socket: "CPU 0" Socket Type: 0x01 (Other) Socket Status: Populated Type: 0x03 (CPU) Family: 0x01 (Other) Manufacturer: "QEMU" Version: "pc-i440fx-4.0" Processor ID: 0x078bfbfd00000663 Status: 0x01 (Enabled) Max. Speed: 2000 MHz Current Speed: 2000 MHz Physical Memory Array: #4096 Use: 0x03 (System memory) Location: 0x01 (Other) Slots: 1 Max. Size: 1 GB ECC: 0x06 (Multi-bit) Memory Device: #4352 Location: "DIMM 0" Manufacturer: "QEMU" Memory Array: #4096 Form Factor: 0x09 (DIMM) Type: 0x07 (RAM) Type Detail: 0x0002 (Other) Data Width: 0 bits Size: 1 GB Memory Array Mapping: #4864 Memory Array: #4096 Partition Width: 1 Start Address: 0x00000000 End Address: 0x40000000 Type 32 Record: #8192 Data 00: 20 0b 00 20 00 00 00 00 00 00 00 Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "03": { "None 00.0": "10105 BIOS", "Note": "Created at bios.186", "Unique ID": "rdCR.lZF+r4EgHp4", "Hardware Class": "bios", "BIOS Keyboard LED Status": { "Scroll Lock": "off", "Num Lock": "off", "Caps Lock": "off", }, "Serial Port 0": "0x3f8", "Parallel Port 0": "0x378", "Base Memory": "639 kB", "PnP BIOS": "@@@0000", "MP spec rev 1.4 info": { "OEM id": "BOCHSCPU", "Product id": "0.1", "Note": "1 CPUs (0 disabled)", }, "BIOS32 Service Directory Entry": "0xfd2b0", "SMBIOS Version": "2.8", "BIOS Info": { "Handle": "#0", "Vendor": "SeaBIOS", "Version": "rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org", "Date": "04/01/2014", "Start Address": "0xe8000", "ROM Size": "64 kB", "Features": ["0x04000000000000000008"], }, "System Info": { "Handle": "#256", "Manufacturer": "QEMU", "Product": "Standard PC (i440FX + PIIX, 1996)", "Version": "pc-i440fx-4.0", "UUID": "undefined", "Wake-up": "0x06 (Power Switch)", }, "Chassis Info": { "Handle": "#768", "Manufacturer": "QEMU", "Version": "pc-i440fx-4.0", "Type": "0x01 (Other)", "Bootup State": "0x03 (Safe)", "Power Supply State": "0x03 (Safe)", "Thermal State": "0x03 (Safe)", "Security Status": "0x02 (Unknown)", }, "Processor Info": { "Handle": "#1024", "Socket": "CPU 0", "Socket Type": "0x01 (Other)", "Socket Status": "Populated", "Type": "0x03 (CPU)", "Family": "0x01 (Other)", "Manufacturer": "QEMU", "Version": "pc-i440fx-4.0", "Processor ID": "0x078bfbfd00000663", "Status": "0x01 (Enabled)", "Max. Speed": "2000 MHz", "Current Speed": "2000 MHz", }, "Physical Memory Array": { "Handle": "#4096", "Use": "0x03 (System memory)", "Location": "0x01 (Other)", "Slots": "1", "Max. Size": "1 GB", "ECC": "0x06 (Multi-bit)", }, "Memory Device": { "Handle": "#4352", "Location": "DIMM 0", "Manufacturer": "QEMU", "Memory Array": {"Handle": "#4096"}, "Form Factor": "0x09 (DIMM)", "Type": "0x07 (RAM)", "Type Detail": "0x0002 (Other)", "Data Width": "0 bits", "Size": "1 GB", }, "Memory Array Mapping": { "Handle": "#4864", "Memory Array": {"Handle": "#4096"}, "Partition Width": "1", "Start Address": "0x00000000", "End Address": "0x40000000", }, "Type 32 Record": { "Handle": "#8192", "Data 00": "20 0b 00 20 00 00 00 00 00 00 00", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_system(self): hwinfo = textwrap.dedent( """ 04: None 00.0: 10107 System [Created at sys.64] Unique ID: rdCR.n_7QNeEnh23 Hardware Class: system Model: "System" Formfactor: "desktop" Driver Info #0: Driver Status: thermal,fan are not active Driver Activation Cmd: "modprobe thermal; modprobe fan" Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "04": { "None 00.0": "10107 System", "Note": "Created at sys.64", "Unique ID": "rdCR.n_7QNeEnh23", "Hardware Class": "system", "Model": "System", "Formfactor": "desktop", "Driver Info #0": { "Driver Status": "thermal,fan are not active", "Driver Activation Cmd": "modprobe thermal; modprobe fan", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_unknown(self): hwinfo = textwrap.dedent( """ 05: None 00.0: 10104 FPU [Created at misc.191] Unique ID: rdCR.EMpH5pjcahD Hardware Class: unknown Model: "FPU" I/O Ports: 0xf0-0xff (rw) Config Status: cfg=new, avail=yes, need=no, active=unknown 06: None 00.0: 0801 DMA controller (8237) [Created at misc.205] Unique ID: rdCR.f5u1ucRm+H9 Hardware Class: unknown Model: "DMA controller" I/O Ports: 0x00-0xcf7 (rw) I/O Ports: 0xc0-0xdf (rw) I/O Ports: 0x80-0x8f (rw) DMA: 4 Config Status: cfg=new, avail=yes, need=no, active=unknown 07: None 00.0: 0800 PIC (8259) [Created at misc.218] Unique ID: rdCR.8uRK7LxiIA2 Hardware Class: unknown Model: "PIC" I/O Ports: 0x20-0x21 (rw) I/O Ports: 0xa0-0xa1 (rw) Config Status: cfg=new, avail=yes, need=no, active=unknown 08: None 00.0: 0900 Keyboard controller [Created at misc.250] Unique ID: rdCR.9N+EecqykME Hardware Class: unknown Model: "Keyboard controller" I/O Port: 0x60 (rw) I/O Port: 0x64 (rw) Config Status: cfg=new, avail=yes, need=no, active=unknown 09: None 00.0: 0701 Parallel controller (SPP) [Created at misc.261] Unique ID: YMnp.ecK7NLYWZ5D Hardware Class: unknown Model: "Parallel controller" Device File: /dev/lp0 I/O Ports: 0x378-0x37a (rw) I/O Ports: 0x37b-0x37f (rw) Config Status: cfg=new, avail=yes, need=no, active=unknown 10: None 00.0: 10400 PS/2 Controller [Created at misc.303] Unique ID: rdCR.DziBbWO85o5 Hardware Class: unknown Model: "PS/2 Controller" Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "05": { "None 00.0": "10104 FPU", "Note": "Created at misc.191", "Unique ID": "rdCR.EMpH5pjcahD", "Hardware Class": "unknown", "Model": "FPU", "I/O Ports": "0xf0-0xff (rw)", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "06": { "None 00.0": "0801 DMA controller (8237)", "Note": "Created at misc.205", "Unique ID": "rdCR.f5u1ucRm+H9", "Hardware Class": "unknown", "Model": "DMA controller", "I/O Ports": [ "0x00-0xcf7 (rw)", "0xc0-0xdf (rw)", "0x80-0x8f (rw)", ], "DMA": "4", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "07": { "None 00.0": "0800 PIC (8259)", "Note": "Created at misc.218", "Unique ID": "rdCR.8uRK7LxiIA2", "Hardware Class": "unknown", "Model": "PIC", "I/O Ports": ["0x20-0x21 (rw)", "0xa0-0xa1 (rw)"], "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "08": { "None 00.0": "0900 Keyboard controller", "Note": "Created at misc.250", "Unique ID": "rdCR.9N+EecqykME", "Hardware Class": "unknown", "Model": "Keyboard controller", "I/O Ports": ["0x60 (rw)", "0x64 (rw)"], "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "09": { "None 00.0": "0701 Parallel controller (SPP)", "Note": "Created at misc.261", "Unique ID": "YMnp.ecK7NLYWZ5D", "Hardware Class": "unknown", "Model": "Parallel controller", "Device File": "/dev/lp0", "I/O Ports": ["0x378-0x37a (rw)", "0x37b-0x37f (rw)"], "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "10": { "None 00.0": "10400 PS/2 Controller", "Note": "Created at misc.303", "Unique ID": "rdCR.DziBbWO85o5", "Hardware Class": "unknown", "Model": "PS/2 Controller", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_memory(self): hwinfo = textwrap.dedent( """ 12: None 00.0: 10102 Main Memory [Created at memory.74] Unique ID: rdCR.CxwsZFjVASF Hardware Class: memory Model: "Main Memory" Memory Range: 0x00000000-0x3cefffff (rw) Memory Size: 960 MB Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "12": { "None 00.0": "10102 Main Memory", "Note": "Created at memory.74", "Unique ID": "rdCR.CxwsZFjVASF", "Hardware Class": "memory", "Model": "Main Memory", "Memory Range": "0x00000000-0x3cefffff (rw)", "Memory Size": "960 MB", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_bridge(self): hwinfo = textwrap.dedent( """ 13: PCI 01.0: 0601 ISA bridge [Created at pci.386] Unique ID: vSkL.ucdhKwLeeAA SysFS ID: /devices/pci0000:00/0000:00:01.0 SysFS BusID: 0000:00:01.0 Hardware Class: bridge Model: "Red Hat Qemu virtual machine" Vendor: pci 0x8086 "Intel Corporation" Device: pci 0x7000 "82371SB PIIX3 ISA [Natoma/Triton II]" SubVendor: pci 0x1af4 "Red Hat, Inc." SubDevice: pci 0x1100 "Qemu virtual machine" Module Alias: "pci:v00008086d00007000sv00001AF4sd00001100bc06sc01i00" Config Status: cfg=new, avail=yes, need=no, active=unknown 14: PCI 00.0: 0600 Host bridge [Created at pci.386] Unique ID: qLht.YeL3TKDjrxE SysFS ID: /devices/pci0000:00/0000:00:00.0 SysFS BusID: 0000:00:00.0 Hardware Class: bridge Model: "Red Hat Qemu virtual machine" Vendor: pci 0x8086 "Intel Corporation" Device: pci 0x1237 "440FX - 82441FX PMC [Natoma]" SubVendor: pci 0x1af4 "Red Hat, Inc." SubDevice: pci 0x1100 "Qemu virtual machine" Revision: 0x02 Module Alias: "pci:v00008086d00001237sv00001AF4sd00001100bc06sc00i00" Config Status: cfg=new, avail=yes, need=no, active=unknown 15: PCI 01.3: 0680 Bridge [Created at pci.386] Unique ID: VRCs.M9Cc8lcQjE2 SysFS ID: /devices/pci0000:00/0000:00:01.3 SysFS BusID: 0000:00:01.3 Hardware Class: bridge Model: "Red Hat Qemu virtual machine" Vendor: pci 0x8086 "Intel Corporation" Device: pci 0x7113 "82371AB/EB/MB PIIX4 ACPI" SubVendor: pci 0x1af4 "Red Hat, Inc." SubDevice: pci 0x1100 "Qemu virtual machine" Revision: 0x03 Driver: "piix4_smbus" Driver Modules: "i2c_piix4" IRQ: 9 (no events) Module Alias: "pci:v00008086d00007113sv00001AF4sd00001100bc06sc80i00" Driver Info #0: Driver Status: i2c_piix4 is active Driver Activation Cmd: "modprobe i2c_piix4" Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "13": { "PCI 01.0": "0601 ISA bridge", "Note": "Created at pci.386", "Unique ID": "vSkL.ucdhKwLeeAA", "SysFS ID": "/devices/pci0000:00/0000:00:01.0", "SysFS BusID": "0000:00:01.0", "Hardware Class": "bridge", "Model": "Red Hat Qemu virtual machine", "Vendor": 'pci 0x8086 "Intel Corporation"', "Device": 'pci 0x7000 "82371SB PIIX3 ISA [Natoma/Triton II]"', "SubVendor": 'pci 0x1af4 "Red Hat, Inc."', "SubDevice": 'pci 0x1100 "Qemu virtual machine"', "Module Alias": "pci:v00008086d00007000sv00001AF4sd00001100bc06sc01i00", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "14": { "PCI 00.0": "0600 Host bridge", "Note": "Created at pci.386", "Unique ID": "qLht.YeL3TKDjrxE", "SysFS ID": "/devices/pci0000:00/0000:00:00.0", "SysFS BusID": "0000:00:00.0", "Hardware Class": "bridge", "Model": "Red Hat Qemu virtual machine", "Vendor": 'pci 0x8086 "Intel Corporation"', "Device": 'pci 0x1237 "440FX - 82441FX PMC [Natoma]"', "SubVendor": 'pci 0x1af4 "Red Hat, Inc."', "SubDevice": 'pci 0x1100 "Qemu virtual machine"', "Revision": "0x02", "Module Alias": "pci:v00008086d00001237sv00001AF4sd00001100bc06sc00i00", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "15": { "PCI 01.3": "0680 Bridge", "Note": "Created at pci.386", "Unique ID": "VRCs.M9Cc8lcQjE2", "SysFS ID": "/devices/pci0000:00/0000:00:01.3", "SysFS BusID": "0000:00:01.3", "Hardware Class": "bridge", "Model": "Red Hat Qemu virtual machine", "Vendor": 'pci 0x8086 "Intel Corporation"', "Device": 'pci 0x7113 "82371AB/EB/MB PIIX4 ACPI"', "SubVendor": 'pci 0x1af4 "Red Hat, Inc."', "SubDevice": 'pci 0x1100 "Qemu virtual machine"', "Revision": "0x03", "Driver": ["piix4_smbus"], "Driver Modules": ["i2c_piix4"], "IRQ": "9 (no events)", "Module Alias": "pci:v00008086d00007113sv00001AF4sd00001100bc06sc80i00", "Driver Info #0": { "Driver Status": "i2c_piix4 is active", "Driver Activation Cmd": "modprobe i2c_piix4", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_ethernet(self): hwinfo = textwrap.dedent( """ 16: PCI 03.0: 0200 Ethernet controller [Created at pci.386] Unique ID: 3hqH.pkM7KXDR457 SysFS ID: /devices/pci0000:00/0000:00:03.0 SysFS BusID: 0000:00:03.0 Hardware Class: unknown Model: "Red Hat Virtio network device" Vendor: pci 0x1af4 "Red Hat, Inc." Device: pci 0x1000 "Virtio network device" SubVendor: pci 0x1af4 "Red Hat, Inc." SubDevice: pci 0x0001 Driver: "virtio-pci" Driver Modules: "virtio_pci" I/O Ports: 0xc000-0xc01f (rw) Memory Range: 0xfebd1000-0xfebd1fff (rw,non-prefetchable) Memory Range: 0xfe000000-0xfe003fff (ro,non-prefetchable) Memory Range: 0xfeb80000-0xfebbffff (ro,non-prefetchable,disabled) IRQ: 11 (no events) Module Alias: "pci:v00001AF4d00001000sv00001AF4sd00000001bc02sc00i00" Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "16": { "PCI 03.0": "0200 Ethernet controller", "Note": "Created at pci.386", "Unique ID": "3hqH.pkM7KXDR457", "SysFS ID": "/devices/pci0000:00/0000:00:03.0", "SysFS BusID": "0000:00:03.0", "Hardware Class": "unknown", "Model": "Red Hat Virtio network device", "Vendor": 'pci 0x1af4 "Red Hat, Inc."', "Device": 'pci 0x1000 "Virtio network device"', "SubVendor": 'pci 0x1af4 "Red Hat, Inc."', "SubDevice": "pci 0x0001", "Driver": ["virtio-pci"], "Driver Modules": ["virtio_pci"], "I/O Ports": "0xc000-0xc01f (rw)", "Memory Range": [ "0xfebd1000-0xfebd1fff (rw,non-prefetchable)", "0xfe000000-0xfe003fff (ro,non-prefetchable)", "0xfeb80000-0xfebbffff (ro,non-prefetchable,disabled)", ], "IRQ": "11 (no events)", "Module Alias": "pci:v00001AF4d00001000sv00001AF4sd00000001bc02sc00i00", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_storage(self): hwinfo = textwrap.dedent( """ 17: PCI 01.1: 0101 IDE interface (ISA Compatibility mode-only controller, supports bus mts bus mastering) [Created at pci.386] Unique ID: mnDB.3sKqaxiizg6 SysFS ID: /devices/pci0000:00/0000:00:01.1 SysFS BusID: 0000:00:01.1 Hardware Class: storage Model: "Red Hat Qemu virtual machine" Vendor: pci 0x8086 "Intel Corporation" Device: pci 0x7010 "82371SB PIIX3 IDE [Natoma/Triton II]" SubVendor: pci 0x1af4 "Red Hat, Inc." SubDevice: pci 0x1100 "Qemu virtual machine" Driver: "ata_piix" Driver Modules: "ata_piix" I/O Ports: 0x1f0-0x1f7 (rw) I/O Port: 0x3f6 (rw) I/O Ports: 0x170-0x177 (rw) I/O Port: 0x376 (rw) I/O Ports: 0xc020-0xc02f (rw) Module Alias: "pci:v00008086d00007010sv00001AF4sd00001100bc01sc01i80" Driver Info #0: Driver Status: ata_piix is active Driver Activation Cmd: "modprobe ata_piix" Driver Info #1: Driver Status: ata_generic is active Driver Activation Cmd: "modprobe ata_generic" Driver Info #2: Driver Status: pata_acpi is active Driver Activation Cmd: "modprobe pata_acpi" Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "17": { "PCI 01.1": "0101 IDE interface (ISA Compatibility mode-only controller, supports bus mts bus mastering)", "Note": "Created at pci.386", "Unique ID": "mnDB.3sKqaxiizg6", "SysFS ID": "/devices/pci0000:00/0000:00:01.1", "SysFS BusID": "0000:00:01.1", "Hardware Class": "storage", "Model": "Red Hat Qemu virtual machine", "Vendor": 'pci 0x8086 "Intel Corporation"', "Device": 'pci 0x7010 "82371SB PIIX3 IDE [Natoma/Triton II]"', "SubVendor": 'pci 0x1af4 "Red Hat, Inc."', "SubDevice": 'pci 0x1100 "Qemu virtual machine"', "Driver": ["ata_piix"], "Driver Modules": ["ata_piix"], "I/O Ports": [ "0x1f0-0x1f7 (rw)", "0x3f6 (rw)", "0x170-0x177 (rw)", "0x376 (rw)", "0xc020-0xc02f (rw)", ], "Module Alias": "pci:v00008086d00007010sv00001AF4sd00001100bc01sc01i80", "Driver Info #0": { "Driver Status": "ata_piix is active", "Driver Activation Cmd": "modprobe ata_piix", }, "Driver Info #1": { "Driver Status": "ata_generic is active", "Driver Activation Cmd": "modprobe ata_generic", }, "Driver Info #2": { "Driver Status": "pata_acpi is active", "Driver Activation Cmd": "modprobe pata_acpi", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_video(self): hwinfo = textwrap.dedent( """ 18: PCI 02.0: 0300 VGA compatible controller (VGA) [Created at pci.386] Unique ID: _Znp.WspiKb87LiA SysFS ID: /devices/pci0000:00/0000:00:02.0 SysFS BusID: 0000:00:02.0 Hardware Class: graphics card Model: "VGA compatible controller" Vendor: pci 0x1234 Device: pci 0x1111 SubVendor: pci 0x1af4 "Red Hat, Inc." SubDevice: pci 0x1100 Revision: 0x02 Driver: "bochs-drm" Driver Modules: "bochs_drm" Memory Range: 0xfd000000-0xfdffffff (ro,non-prefetchable) Memory Range: 0xfebd0000-0xfebd0fff (rw,non-prefetchable) Memory Range: 0x000c0000-0x000dffff (rw,non-prefetchable,disabled) Module Alias: "pci:v00001234d00001111sv00001AF4sd00001100bc03sc00i00" Driver Info #0: Driver Status: bochs_drm is active Driver Activation Cmd: "modprobe bochs_drm" Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "18": { "PCI 02.0": "0300 VGA compatible controller (VGA)", "Note": "Created at pci.386", "Unique ID": "_Znp.WspiKb87LiA", "SysFS ID": "/devices/pci0000:00/0000:00:02.0", "SysFS BusID": "0000:00:02.0", "Hardware Class": "graphics card", "Model": "VGA compatible controller", "Vendor": "pci 0x1234", "Device": "pci 0x1111", "SubVendor": 'pci 0x1af4 "Red Hat, Inc."', "SubDevice": "pci 0x1100", "Revision": "0x02", "Driver": ["bochs-drm"], "Driver Modules": ["bochs_drm"], "Memory Range": [ "0xfd000000-0xfdffffff (ro,non-prefetchable)", "0xfebd0000-0xfebd0fff (rw,non-prefetchable)", "0x000c0000-0x000dffff (rw,non-prefetchable,disabled)", ], "Module Alias": "pci:v00001234d00001111sv00001AF4sd00001100bc03sc00i00", "Driver Info #0": { "Driver Status": "bochs_drm is active", "Driver Activation Cmd": "modprobe bochs_drm", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_network(self): hwinfo = textwrap.dedent( """ 19: Virtio 00.0: 0200 Ethernet controller [Created at pci.1679] Unique ID: vWuh.VIRhsc57kTD Parent ID: 3hqH.pkM7KXDR457 SysFS ID: /devices/pci0000:00/0000:00:03.0/virtio0 SysFS BusID: virtio0 Hardware Class: network Model: "Virtio Ethernet Card 0" Vendor: int 0x6014 "Virtio" Device: int 0x0001 "Ethernet Card 0" Driver: "virtio_net" Driver Modules: "virtio_net" Device File: ens3 HW Address: 52:54:00:12:34:56 Permanent HW Address: 52:54:00:12:34:56 Link detected: yes Module Alias: "virtio:d00000001v00001AF4" Driver Info #0: Driver Status: virtio_net is active Driver Activation Cmd: "modprobe virtio_net" Config Status: cfg=new, avail=yes, need=no, active=unknown Attached to: #16 (Ethernet controller) 20: None 00.0: 0700 Serial controller (16550) [Created at serial.74] Unique ID: S_Uw.3fyvFV+mbWD Hardware Class: unknown Model: "16550A" Device: "16550A" Device File: /dev/ttyS0 Tags: mouse, modem, braille I/O Ports: 0x3f8-0x3ff (rw) IRQ: 4 (55234 events) Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "19": { "Virtio 00.0": "0200 Ethernet controller", "Note": "Created at pci.1679", "Unique ID": "vWuh.VIRhsc57kTD", "Parent ID": "3hqH.pkM7KXDR457", "SysFS ID": "/devices/pci0000:00/0000:00:03.0/virtio0", "SysFS BusID": "virtio0", "Hardware Class": "network", "Model": "Virtio Ethernet Card 0", "Vendor": 'int 0x6014 "Virtio"', "Device": 'int 0x0001 "Ethernet Card 0"', "Driver": ["virtio_net"], "Driver Modules": ["virtio_net"], "Device File": "ens3", "HW Address": "52:54:00:12:34:56", "Permanent HW Address": "52:54:00:12:34:56", "Link detected": "yes", "Module Alias": "virtio:d00000001v00001AF4", "Driver Info #0": { "Driver Status": "virtio_net is active", "Driver Activation Cmd": "modprobe virtio_net", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, "Attached to": {"Handle": "#16 (Ethernet controller)"}, }, "20": { "None 00.0": "0700 Serial controller (16550)", "Note": "Created at serial.74", "Unique ID": "S_Uw.3fyvFV+mbWD", "Hardware Class": "unknown", "Model": "16550A", "Device": "16550A", "Device File": "/dev/ttyS0", "Tags": ["mouse", "modem", "braille"], "I/O Ports": "0x3f8-0x3ff (rw)", "IRQ": "4 (55234 events)", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_disk(self): hwinfo = textwrap.dedent( """ 21: SCSI 100.0: 10602 CD-ROM (DVD) [Created at block.249] Unique ID: KD9E.53N0UD4ozwD Parent ID: mnDB.3sKqaxiizg6 SysFS ID: /class/block/sr0 SysFS BusID: 1:0:0:0 SysFS Device Link: /devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0 Hardware Class: cdrom Model: "QEMU DVD-ROM" Vendor: "QEMU" Device: "QEMU DVD-ROM" Revision: "2.5+" Driver: "ata_piix", "sr" Driver Modules: "ata_piix", "sr_mod" Device File: /dev/sr0 (/dev/sg1) Device Files: /dev/sr0, /dev/cdrom, /dev/dvd, /dev/disk/by-path/pci-0000:00:01.1-ata-2, /dev/disk/by-id/ata-QEMU_DVD-ROM_QM00003, /dev/disk/by-uuid/2019-08-11-11-44-39-00, /dev/disk/by-label/CDROM Device Number: block 11:0 (char 21:1) Features: DVD, MRW, MRW-W Config Status: cfg=new, avail=yes, need=no, active=unknown Attached to: #17 (IDE interface) Drive Speed: 4 Volume ID: "CDROM" Application: "0X5228779D" Publisher: "SUSE LLC" Preparer: "KIWI - HTTPS://GITHUB.COM/OSINSIDE/KIWI" Creation date: "2019081111443900" El Torito info: platform 0, bootable Boot Catalog: at sector 0x00fa Media: none starting at sector 0x00fb Load: 2048 bytes 22: None 00.0: 10600 Disk [Created at block.245] Unique ID: kwWm.Fxp0d3BezAE SysFS ID: /class/block/fd0 SysFS BusID: floppy.0 SysFS Device Link: /devices/platform/floppy.0 Hardware Class: disk Model: "Disk" Driver: "floppy" Driver Modules: "floppy" Device File: /dev/fd0 Device Number: block 2:0 Size: 8 sectors a 512 bytes Capacity: 0 GB (4096 bytes) Drive status: no medium Config Status: cfg=new, avail=yes, need=no, active=unknown 23: IDE 00.0: 10600 Disk [Created at block.245] Unique ID: 3OOL.W8iGvCekDp8 Parent ID: mnDB.3sKqaxiizg6 SysFS ID: /class/block/sda SysFS BusID: 0:0:0:0 SysFS Device Link: /devices/pci0000:00/0000:00:01.1/ata1/host0/target0:0:0/0:0:0:0 Hardware Class: disk Model: "QEMU HARDDISK" Vendor: "QEMU" Device: "HARDDISK" Revision: "2.5+" Serial ID: "QM00001" Driver: "ata_piix", "sd" Driver Modules: "ata_piix" Device File: /dev/sda Device Files: /dev/sda, /dev/disk/by-path/pci-0000:00:01.1-ata-1, /dev/disk/by-id/ata-QEMU_HARDDISK_QM00001 Device Number: block 8:0-8:15 Geometry (Logical): CHS 3133/255/63 Size: 50331648 sectors a 512 bytes Capacity: 24 GB (25769803776 bytes) Config Status: cfg=new, avail=yes, need=no, active=unknown Attached to: #17 (IDE interface) """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "21": { "SCSI 100.0": "10602 CD-ROM (DVD)", "Note": "Created at block.249", "Unique ID": "KD9E.53N0UD4ozwD", "Parent ID": "mnDB.3sKqaxiizg6", "SysFS ID": "/class/block/sr0", "SysFS BusID": "1:0:0:0", "SysFS Device Link": "/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0", "Hardware Class": "cdrom", "Model": "QEMU DVD-ROM", "Vendor": "QEMU", "Device": "QEMU DVD-ROM", "Revision": "2.5+", "Driver": ["ata_piix", "sr"], "Driver Modules": ["ata_piix", "sr_mod"], "Device File": "/dev/sr0 (/dev/sg1)", "Device Files": [ "/dev/sr0", "/dev/cdrom", "/dev/dvd", "/dev/disk/by-path/pci-0000:00:01.1-ata-2", "/dev/disk/by-id/ata-QEMU_DVD-ROM_QM00003", "/dev/disk/by-uuid/2019-08-11-11-44-39-00", "/dev/disk/by-label/CDROM", ], "Device Number": "block 11:0 (char 21:1)", "Features": ["DVD", "MRW", "MRW-W"], "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, "Attached to": {"Handle": "#17 (IDE interface)"}, "Drive Speed": "4", "Volume ID": "CDROM", "Application": "0X5228779D", "Publisher": "SUSE LLC", "Preparer": "KIWI - HTTPS://GITHUB.COM/OSINSIDE/KIWI", "Creation date": "2019081111443900", "El Torito info": { "platform": "0", "bootable": "yes", "Boot Catalog": "at sector 0x00fa", "Media": "none starting at sector 0x00fb", "Load": "2048 bytes", }, }, "22": { "None 00.0": "10600 Disk", "Note": "Created at block.245", "Unique ID": "kwWm.Fxp0d3BezAE", "SysFS ID": "/class/block/fd0", "SysFS BusID": "floppy.0", "SysFS Device Link": "/devices/platform/floppy.0", "Hardware Class": "disk", "Model": "Disk", "Driver": ["floppy"], "Driver Modules": ["floppy"], "Device File": "/dev/fd0", "Device Number": "block 2:0", "Size": "8 sectors a 512 bytes", "Capacity": "0 GB (4096 bytes)", "Drive status": "no medium", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "23": { "IDE 00.0": "10600 Disk", "Note": "Created at block.245", "Unique ID": "3OOL.W8iGvCekDp8", "Parent ID": "mnDB.3sKqaxiizg6", "SysFS ID": "/class/block/sda", "SysFS BusID": "0:0:0:0", "SysFS Device Link": "/devices/pci0000:00/0000:00:01.1/ata1/host0/target0:0:0/0:0:0:0", "Hardware Class": "disk", "Model": "QEMU HARDDISK", "Vendor": "QEMU", "Device": "HARDDISK", "Revision": "2.5+", "Serial ID": "QM00001", "Driver": ["ata_piix", "sd"], "Driver Modules": ["ata_piix"], "Device File": "/dev/sda", "Device Files": [ "/dev/sda", "/dev/disk/by-path/pci-0000:00:01.1-ata-1", "/dev/disk/by-id/ata-QEMU_HARDDISK_QM00001", ], "Device Number": "block 8:0-8:15", "Geometry (Logical)": "CHS 3133/255/63", "Size": "50331648 sectors a 512 bytes", "Capacity": "24 GB (25769803776 bytes)", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, "Attached to": {"Handle": "#17 (IDE interface)"}, }, }, ) def test__hwinfo_parse_full_keyboard(self): hwinfo = textwrap.dedent( """ 24: PS/2 00.0: 10800 Keyboard [Created at input.226] Unique ID: nLyy.+49ps10DtUF Hardware Class: keyboard Model: "AT Translated Set 2 keyboard" Vendor: 0x0001 Device: 0x0001 "AT Translated Set 2 keyboard" Compatible to: int 0x0211 0x0001 Device File: /dev/input/event0 Device Files: /dev/input/event0, /dev/input/by-path/platform-i8042-serio-0-event-kbd Device Number: char 13:64 Driver Info #0: XkbRules: xfree86 XkbModel: pc104 Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "24": { "PS/2 00.0": "10800 Keyboard", "Note": "Created at input.226", "Unique ID": "nLyy.+49ps10DtUF", "Hardware Class": "keyboard", "Model": "AT Translated Set 2 keyboard", "Vendor": "0x0001", "Device": '0x0001 "AT Translated Set 2 keyboard"', "Compatible to": "int 0x0211 0x0001", "Device File": "/dev/input/event0", "Device Files": [ "/dev/input/event0", "/dev/input/by-path/platform-i8042-serio-0-event-kbd", ], "Device Number": "char 13:64", "Driver Info #0": {"XkbRules": "xfree86", "XkbModel": "pc104"}, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_mouse(self): hwinfo = textwrap.dedent( """ 25: PS/2 00.0: 10500 PS/2 Mouse [Created at input.249] Unique ID: AH6Q.mYF0pYoTCW7 Hardware Class: mouse Model: "VirtualPS/2 VMware VMMouse" Vendor: 0x0002 Device: 0x0013 "VirtualPS/2 VMware VMMouse" Compatible to: int 0x0210 0x0003 Device File: /dev/input/mice (/dev/input/mouse0) Device Files: /dev/input/mice, /dev/input/mouse0, /dev/input/event1, /dev/input/by-path/platform-i8042-serio-1-event-mouse, /dev/input/by-path/platform-i8042-serio-1-mouse Device Number: char 13:63 (char 13:32) Driver Info #0: Buttons: 3 Wheels: 0 XFree86 Protocol: explorerps/2 GPM Protocol: exps2 Config Status: cfg=new, avail=yes, need=no, active=unknown 26: PS/2 00.0: 10500 PS/2 Mouse [Created at input.249] Unique ID: AH6Q.++hSeDccb2F Hardware Class: mouse Model: "VirtualPS/2 VMware VMMouse" Vendor: 0x0002 Device: 0x0013 "VirtualPS/2 VMware VMMouse" Compatible to: int 0x0210 0x0012 Device File: /dev/input/mice (/dev/input/mouse1) Device Files: /dev/input/mice, /dev/input/mouse1, /dev/input/event2 Device Number: char 13:63 (char 13:33) Driver Info #0: Buttons: 2 Wheels: 1 XFree86 Protocol: explorerps/2 GPM Protocol: exps2 Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "25": { "PS/2 00.0": "10500 PS/2 Mouse", "Note": "Created at input.249", "Unique ID": "AH6Q.mYF0pYoTCW7", "Hardware Class": "mouse", "Model": "VirtualPS/2 VMware VMMouse", "Vendor": "0x0002", "Device": '0x0013 "VirtualPS/2 VMware VMMouse"', "Compatible to": "int 0x0210 0x0003", "Device File": "/dev/input/mice (/dev/input/mouse0)", "Device Files": [ "/dev/input/mice", "/dev/input/mouse0", "/dev/input/event1", "/dev/input/by-path/platform-i8042-serio-1-event-mouse", "/dev/input/by-path/platform-i8042-serio-1-mouse", ], "Device Number": "char 13:63 (char 13:32)", "Driver Info #0": { "Buttons": "3", "Wheels": "0", "XFree86 Protocol": "explorerps/2", "GPM Protocol": "exps2", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "26": { "PS/2 00.0": "10500 PS/2 Mouse", "Note": "Created at input.249", "Unique ID": "AH6Q.++hSeDccb2F", "Hardware Class": "mouse", "Model": "VirtualPS/2 VMware VMMouse", "Vendor": "0x0002", "Device": '0x0013 "VirtualPS/2 VMware VMMouse"', "Compatible to": "int 0x0210 0x0012", "Device File": "/dev/input/mice (/dev/input/mouse1)", "Device Files": [ "/dev/input/mice", "/dev/input/mouse1", "/dev/input/event2", ], "Device Number": "char 13:63 (char 13:33)", "Driver Info #0": { "Buttons": "2", "Wheels": "1", "XFree86 Protocol": "explorerps/2", "GPM Protocol": "exps2", }, "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_cpu(self): hwinfo = textwrap.dedent( """ 27: None 00.0: 10103 CPU [Created at cpu.462] Unique ID: rdCR.j8NaKXDZtZ6 Hardware Class: cpu Arch: X86-64 Vendor: "GenuineIntel" Model: 6.6.3 "QEMU Virtual CPU version 2.5+" Features: fpu,de,pse,tsc,msr,pae,mce,cx8,apic,sep,mtrr,pge,mca,cmov,pse36,clflush,mmx,fxsr,sse,sse2,syscall,nx,lm,rep_good,nopl,xtopology,cpuid,tsc_known_freq,pni,cx16,x2apic,hypervisor,lahf_lm,cpuid_fault,pti Clock: 3591 MHz BogoMips: 7182.68 Cache: 16384 kb Config Status: cfg=new, avail=yes, need=no, active=unknown """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "27": { "None 00.0": "10103 CPU", "Note": "Created at cpu.462", "Unique ID": "rdCR.j8NaKXDZtZ6", "Hardware Class": "cpu", "Arch": "X86-64", "Vendor": "GenuineIntel", "Model": '6.6.3 "QEMU Virtual CPU version 2.5+"', "Features": [ "fpu", "de", "pse", "tsc", "msr", "pae", "mce", "cx8", "apic", "sep", "mtrr", "pge", "mca", "cmov", "pse36", "clflush", "mmx", "fxsr", "sse", "sse2", "syscall", "nx", "lm", "rep_good", "nopl", "xtopology", "cpuid", "tsc_known_freq", "pni", "cx16", "x2apic", "hypervisor", "lahf_lm", "cpuid_fault", "pti", ], "Clock": "3591 MHz", "BogoMips": "7182.68", "Cache": "16384 kb", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, }, ) def test__hwinfo_parse_full_nic(self): hwinfo = textwrap.dedent( """ 28: None 00.0: 10700 Loopback [Created at net.126] Unique ID: ZsBS.GQNx7L4uPNA SysFS ID: /class/net/lo Hardware Class: network interface Model: "Loopback network interface" Device File: lo Link detected: yes Config Status: cfg=new, avail=yes, need=no, active=unknown 29: None 03.0: 10701 Ethernet [Created at net.126] Unique ID: U2Mp.ndpeucax6V1 Parent ID: vWuh.VIRhsc57kTD SysFS ID: /class/net/ens3 SysFS Device Link: /devices/pci0000:00/0000:00:03.0/virtio0 Hardware Class: network interface Model: "Ethernet network interface" Driver: "virtio_net" Driver Modules: "virtio_net" Device File: ens3 HW Address: 52:54:00:12:34:56 Permanent HW Address: 52:54:00:12:34:56 Link detected: yes Config Status: cfg=new, avail=yes, need=no, active=unknown Attached to: #19 (Ethernet controller) """ ) self.assertEqual( devices._hwinfo_parse_full(hwinfo), { "28": { "None 00.0": "10700 Loopback", "Note": "Created at net.126", "Unique ID": "ZsBS.GQNx7L4uPNA", "SysFS ID": "/class/net/lo", "Hardware Class": "network interface", "Model": "Loopback network interface", "Device File": "lo", "Link detected": "yes", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, }, "29": { "None 03.0": "10701 Ethernet", "Note": "Created at net.126", "Unique ID": "U2Mp.ndpeucax6V1", "Parent ID": "vWuh.VIRhsc57kTD", "SysFS ID": "/class/net/ens3", "SysFS Device Link": "/devices/pci0000:00/0000:00:03.0/virtio0", "Hardware Class": "network interface", "Model": "Ethernet network interface", "Driver": ["virtio_net"], "Driver Modules": ["virtio_net"], "Device File": "ens3", "HW Address": "52:54:00:12:34:56", "Permanent HW Address": "52:54:00:12:34:56", "Link detected": "yes", "Config Status": { "cfg": "new", "avail": "yes", "need": "no", "active": "unknown", }, "Attached to": {"Handle": "#19 (Ethernet controller)"}, }, }, ) 07070100000087000081A40000000000000000000000016130D1CF00000530000000000000000000000000000000000000003500000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_disk.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import unittest from utils import disk class DiskTestCase(unittest.TestCase): def test_units(self): self.assertEqual(disk.units(1), (1, "MB")) self.assertEqual(disk.units("1"), (1, "MB")) self.assertEqual(disk.units("1.0"), (1, "MB")) self.assertEqual(disk.units("1s"), (1, "s")) self.assertEqual(disk.units("1.1s"), (1.1, "s")) self.assertRaises(disk.ParseException, disk.units, "s1") 07070100000088000081A40000000000000000000000016130D1CF0000332E000000000000000000000000000000000000003700000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_images.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import unittest from unittest.mock import patch, MagicMock from salt.exceptions import SaltInvocationError, CommandExecutionError from modules import images class ImagesTestCase(unittest.TestCase): def test__checksum_url(self): """Test images._checksum_url function""" self.assertEqual( images._checksum_url("http://example.com/image.xz", "md5"), "http://example.com/image.md5", ) self.assertEqual( images._checksum_url("http://example.com/image.ext4", "md5"), "http://example.com/image.ext4.md5", ) def test__curl_cmd(self): """Test images._curl_cmd function""" self.assertEqual( images._curl_cmd("http://example.com/image.xz"), ["curl", "http://example.com/image.xz"], ) self.assertEqual( images._curl_cmd("http://example.com/image.xz", s=None), ["curl", "-s", "http://example.com/image.xz"], ) self.assertEqual( images._curl_cmd("http://example.com/image.xz", s="a"), ["curl", "-s", "a", "http://example.com/image.xz"], ) self.assertEqual( images._curl_cmd("http://example.com/image.xz", _long=None), ["curl", "--_long", "http://example.com/image.xz"], ) self.assertEqual( images._curl_cmd("http://example.com/image.xz", _long="a"), ["curl", "--_long", "a", "http://example.com/image.xz"], ) def test__fetch_file(self): """Test images._fetch_file function""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="stdout"), } with patch.dict(images.__salt__, salt_mock): self.assertEqual(images._fetch_file("http://url"), "stdout") salt_mock["cmd.run_stdout"].assert_called_with( ["curl", "--silent", "--location", "http://url"] ) with patch.dict(images.__salt__, salt_mock): self.assertEqual(images._fetch_file("http://url", s="a"), "stdout") salt_mock["cmd.run_stdout"].assert_called_with( ["curl", "--silent", "--location", "-s", "a", "http://url"] ) def test__find_filesystem(self): """Test images._find_filesystem function""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="ext4"), } with patch.dict(images.__salt__, salt_mock): self.assertEqual(images._find_filesystem("/dev/sda1"), "ext4") salt_mock["cmd.run_stdout"].assert_called_with( ["lsblk", "--noheadings", "--output", "FSTYPE", "/dev/sda1"] ) def test_fetch_checksum(self): """Test images.fetch_checksum function""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="mychecksum -"), } with patch.dict(images.__salt__, salt_mock): self.assertEqual( images.fetch_checksum("http://url/image.xz", checksum_type="md5"), "mychecksum", ) salt_mock["cmd.run_stdout"].assert_called_with( ["curl", "--silent", "--location", "http://url/image.md5"] ) with patch.dict(images.__salt__, salt_mock): self.assertEqual( images.fetch_checksum("http://url/image.ext4", checksum_type="md5"), "mychecksum", ) salt_mock["cmd.run_stdout"].assert_called_with( ["curl", "--silent", "--location", "http://url/image.ext4.md5"] ) with patch.dict(images.__salt__, salt_mock): self.assertEqual( images.fetch_checksum( "http://url/image.xz", checksum_type="sha1", s="a" ), "mychecksum", ) salt_mock["cmd.run_stdout"].assert_called_with( ["curl", "--silent", "--location", "-s", "a", "http://url/image.sha1"] ) def test_dump_invalid_url(self): """Test images.dump function with an invalid URL""" with self.assertRaises(SaltInvocationError): images.dump("random://example.org", "/dev/sda1") def test_dump_invalid_checksum_type(self): """Test images.dump function with an invalid checksum type""" with self.assertRaises(SaltInvocationError): images.dump("http://example.org/image.xz", "/dev/sda1", checksum_type="crc") def test_dump_missing_checksum_type(self): """Test images.dump function with a missing checksum type""" with self.assertRaises(SaltInvocationError): images.dump( "http://example.org/image.xz", "/dev/sda1", checksum="mychecksum" ) def test_dump_download_fail(self): """Test images.dump function when download fails""" salt_mock = { "cmd.run_all": MagicMock(return_value={"retcode": 1, "stderr": "error"}), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump("http://example.org/image.ext4", "/dev/sda1") salt_mock["cmd.run_all"].assert_called_with( "set -eo pipefail ; curl --fail --location --silent " "http://example.org/image.ext4 | tee /dev/sda1 " "| md5sum", python_shell=True, ) def test_dump_download_fail_gz(self): """Test images.dump function when download fails (gz)""" salt_mock = { "cmd.run_all": MagicMock(return_value={"retcode": 1, "stderr": "error"}), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump("http://example.org/image.gz", "/dev/sda1") salt_mock["cmd.run_all"].assert_called_with( "set -eo pipefail ; curl --fail --location --silent " "http://example.org/image.gz | gunzip | tee /dev/sda1 " "| md5sum", python_shell=True, ) def test_dump_download_fail_bz2(self): """Test images.dump function when download fails (bz2)""" salt_mock = { "cmd.run_all": MagicMock(return_value={"retcode": 1, "stderr": "error"}), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump("http://example.org/image.bz2", "/dev/sda1") salt_mock["cmd.run_all"].assert_called_with( "set -eo pipefail ; curl --fail --location --silent " "http://example.org/image.bz2 | bzip2 -d | tee /dev/sda1 " "| md5sum", python_shell=True, ) def test_dump_download_fail_xz(self): """Test images.dump function when download fails (xz)""" salt_mock = { "cmd.run_all": MagicMock(return_value={"retcode": 1, "stderr": "error"}), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump("http://example.org/image.xz", "/dev/sda1") salt_mock["cmd.run_all"].assert_called_with( "set -eo pipefail ; curl --fail --location --silent " "http://example.org/image.xz | xz -d | tee /dev/sda1 " "| md5sum", python_shell=True, ) def test_dump_download_checksum_fail(self): """Test images.dump function when checksum fails""" salt_mock = { "cmd.run_all": MagicMock( return_value={"retcode": 0, "stdout": "badchecksum"} ), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump( "http://example.org/image.ext4", "/dev/sda1", checksum_type="md5", checksum="checksum", ) def test_dump_download_checksum_fail_fetch(self): """Test images.dump function when checksum fails""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="checksum -"), "cmd.run_all": MagicMock( return_value={"retcode": 0, "stdout": "badchecksum"} ), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump( "http://example.org/image.ext4", "/dev/sda1", checksum_type="md5" ) def test_dump_resize_fail_extx(self): """Test images.dump function when resize fails (extx)""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="ext4"), "cmd.run_all": MagicMock( side_effect=[ {"retcode": 0, "stdout": "checksum"}, {"retcode": 1, "stderr": "error"}, ] ), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump( "http://example.org/image.ext4", "/dev/sda1", checksum_type="md5", checksum="checksum", ) salt_mock["cmd.run_all"].assert_called_with( "e2fsck -f -y /dev/sda1; resize2fs /dev/sda1", python_shell=True ) def test_dump_resize_fail_btrfs(self): """Test images.dump function when resize fails (btrfs)""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="btrfs"), "cmd.run_all": MagicMock( side_effect=[ {"retcode": 0, "stdout": "checksum"}, {"retcode": 1, "stderr": "error"}, ] ), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump( "http://example.org/image.btrfs", "/dev/sda1", checksum_type="md5", checksum="checksum", ) salt_mock["cmd.run_all"].assert_called_with( "mount /dev/sda1 /mnt; btrfs filesystem resize max /mnt; " "umount /mnt", python_shell=True, ) def test_dump_resize_fail_xfs(self): """Test images.dump function when resize fails (xfs)""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="xfs"), "cmd.run_all": MagicMock( side_effect=[ {"retcode": 0, "stdout": "checksum"}, {"retcode": 1, "stderr": "error"}, ] ), } with patch.dict(images.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): images.dump( "http://example.org/image.xfs", "/dev/sda1", checksum_type="md5", checksum="checksum", ) salt_mock["cmd.run_all"].assert_called_with( "mount /dev/sda1 /mnt; xfs_growfs /mnt; umount /mnt", python_shell=True ) def test_dump_resize(self): """Test images.dump function""" salt_mock = { "cmd.run_stdout": MagicMock(return_value="ext4"), "cmd.run_all": MagicMock( side_effect=[{"retcode": 0, "stdout": "checksum"}, {"retcode": 0}] ), "cmd.run": MagicMock(return_value=""), } with patch.dict(images.__salt__, salt_mock): self.assertEqual( images.dump( "http://example.org/image.ext4", "/dev/sda1", checksum_type="md5", checksum="checksum", ), "checksum", ) salt_mock["cmd.run"].assert_called_with("sync") 07070100000089000081A40000000000000000000000016130D1CF00005276000000000000000000000000000000000000003300000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_lp.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import unittest from utils import lp class ModelTestCase(unittest.TestCase): def test_add_constraint_fails(self): """Test Model.add_constraint asserts.""" model = lp.Model(["x1", "x2"]) self.assertRaises(AssertionError, model.add_constraint, [1], lp.EQ, 1) self.assertRaises(AssertionError, model.add_constraint, [1, 2, 3], lp.EQ, 1) self.assertRaises(AssertionError, model.add_constraint, [1, 2], None, 1) def test_add_constraint(self): """Test Model.add_constraint success.""" model = lp.Model(["x1", "x2"]) model.add_constraint([1, 2], lp.EQ, 1) self.assertTrue(([1, 2], lp.EQ, 1) in model._constraints) def test_add_cost_function_fails(self): """Test Model.add_cost_function asserts.""" model = lp.Model(["x1", "x2"]) self.assertRaises(AssertionError, model.add_cost_function, None, [1, 2], 1) self.assertRaises(AssertionError, model.add_cost_function, lp.MINIMIZE, [1], 1) self.assertRaises( AssertionError, model.add_cost_function, lp.MINIMIZE, [1, 2, 3], 1 ) def test_add_cost_function(self): """Test Model.add_cost_function success.""" model = lp.Model(["x1", "x2"]) model.add_cost_function(lp.MINIMIZE, [1, 2], 1) self.assertEqual((lp.MINIMIZE, [1, 2], 1), model._cost_function) def test__coeff(self): """Test model._coeff method.""" model = lp.Model(["x1", "x2"]) self.assertEqual(model._coeff({"x1": 1}), [1, 0]) self.assertEqual(model._coeff({"x2": 1}), [0, 1]) self.assertEqual(model._coeff({"x1": 1, "x2": 2}), [1, 2]) def test_add_constraint_named(self): """Test Model.add_constraint_named success.""" model = lp.Model(["x1", "x2"]) model.add_constraint_named({"x1": 1, "x2": 2}, lp.EQ, 1) self.assertTrue(([1, 2], lp.EQ, 1) in model._constraints) def test_add_cost_function_named(self): """Test Model.add_cost_function success.""" model = lp.Model(["x1", "x2"]) model.add_cost_function_named(lp.MINIMIZE, {"x1": 1, "x2": 2}, 1) self.assertEqual((lp.MINIMIZE, [1, 2], 1), model._cost_function) def test_simplex(self): """Test Model.simplex method.""" model = lp.Model(["x1", "x2", "x3", "x4", "x5"]) model.add_constraint([-6, 0, 1, -2, 2], lp.EQ, 6) model.add_constraint([-3, 1, 0, 6, 3], lp.EQ, 15) model.add_cost_function(lp.MINIMIZE, [5, 0, 0, 3, -2], -21) self.assertEqual( model.simplex(), {"x1": 1.0, "x2": 0.0, "x3": 0.0, "x4": 0.0, "x5": 6.0} ) def test__convert_to_standard_form_standard(self): """Test Model._convert_to_standard_form when in standard form.""" model = lp.Model(["x1", "x2", "x3"]) model.add_constraint([30, 100, 85], lp.EQ, 2500) model.add_constraint([6, 2, 3], lp.EQ, 90) model.add_cost_function(lp.MINIMIZE, [3, 2, 4], 0) model._convert_to_standard_form() self.assertEqual(model._slack_variables, []) self.assertEqual( model._standard_constraints, [([30, 100, 85], 2500), ([6, 2, 3], 90)] ) self.assertEqual(model._standard_cost_function, ([3, 2, 4], 0)) def test__convert_to_standard_form_lte(self): """Test Model._convert_to_standard_form when constraint is LTE.""" model = lp.Model(["x1", "x2", "x3"]) model.add_constraint([30, 100, 85], lp.LTE, 2500) model.add_constraint([6, 2, 3], lp.EQ, 90) model.add_cost_function(lp.MINIMIZE, [3, 2, 4], 0) model._convert_to_standard_form() self.assertEqual(model._slack_variables, [3]) self.assertEqual( model._standard_constraints, [([30, 100, 85, 1], 2500), ([6, 2, 3, 0], 90)] ) self.assertEqual(model._standard_cost_function, ([3, 2, 4, 0], 0)) def test__convert_to_standard_form_gte(self): """Test Model._convert_to_standard_form when constraint is GTE.""" model = lp.Model(["x1", "x2", "x3"]) model.add_constraint([30, 100, 85], lp.GTE, 2500) model.add_constraint([6, 2, 3], lp.EQ, 90) model.add_cost_function(lp.MINIMIZE, [3, 2, 4], 0) model._convert_to_standard_form() self.assertEqual(model._slack_variables, [3]) self.assertEqual( model._standard_constraints, [([30, 100, 85, -1], 2500), ([6, 2, 3, 0], 90)] ) self.assertEqual(model._standard_cost_function, ([3, 2, 4, 0], 0)) def test__convert_to_standard_form_lte_gte(self): """Test Model._convert_to_standard_form for LTE/GTE constraints.""" model = lp.Model(["x1", "x2", "x3"]) model.add_constraint([30, 100, 85], lp.LTE, 2500) model.add_constraint([6, 2, 3], lp.GTE, 90) model.add_cost_function(lp.MINIMIZE, [3, 2, 4], 0) model._convert_to_standard_form() self.assertEqual(model._slack_variables, [3, 4]) self.assertEqual( model._standard_constraints, [([30, 100, 85, 1, 0], 2500), ([6, 2, 3, 0, -1], 90)], ) self.assertEqual(model._standard_cost_function, ([3, 2, 4, 0, 0], 0)) def test__convert_to_standard_form_maximize(self): """Test Model._convert_to_standard_form when maximizing.""" model = lp.Model(["x1", "x2", "x3"]) model.add_constraint([30, 100, 85], lp.EQ, 2500) model.add_constraint([6, 2, 3], lp.EQ, 90) model.add_cost_function(lp.MAXIMIZE, [3, 2, 4], 0) model._convert_to_standard_form() self.assertEqual(model._slack_variables, []) self.assertEqual( model._standard_constraints, [([30, 100, 85], 2500), ([6, 2, 3], 90)] ) self.assertEqual(model._standard_cost_function, ([-3, -2, -4], 0)) def test__convert_to_canonical_form(self): """Test Model._convert_to_canonical_form when in standard form.""" model = lp.Model(["x1", "x2", "x3", "x4"]) model.add_constraint([1, -2, -3, -2], lp.EQ, 3) model.add_constraint([1, -1, 2, 1], lp.EQ, 11) model.add_cost_function(lp.MINIMIZE, [2, -3, 1, 1], 0) model._convert_to_standard_form() model._convert_to_canonical_form() self.assertEqual( model._canonical_constraints, [([1, -2, -3, -2, 1, 0], 3), ([1, -1, 2, 1, 0, 1], 11)], ) self.assertEqual(model._canonical_cost_function, ([2, -3, 1, 1, 0, 0], 0)) self.assertEqual( model._canonical_artificial_function, ([-2, 3, 1, 1, 0, 0], -14) ) def test__convert_to_canonical_form_neg_free_term(self): """Test Model._convert_to_standard_form when in standard form.""" model = lp.Model(["x1", "x2", "x3"]) model.add_constraint([30, 100, 85], lp.EQ, -2500) model.add_constraint([6, 2, 3], lp.EQ, 90) model.add_cost_function(lp.MINIMIZE, [3, 2, 4], 0) model._convert_to_standard_form() model._convert_to_canonical_form() self.assertEqual( model._canonical_constraints, [([-30, -100, -85, 1, 0], 2500), ([6, 2, 3, 0, 1], 90)], ) self.assertEqual(model._canonical_cost_function, ([3, 2, 4, 0, 0], 0)) self.assertEqual( model._canonical_artificial_function, ([24, 98, 82, 0, 0], -2590) ) def test__convert_to_canonical_form_artificial(self): """Test Model._convert_to_canonical_form when not in standard form.""" model = lp.Model(["x1", "x2", "x3", "x4"]) model.add_constraint([1, -2, -3, -2], lp.LTE, 3) model.add_constraint([1, -1, 2, 1], lp.GTE, 11) model.add_cost_function(lp.MAXIMIZE, [2, -3, 1, 1], 10) model._convert_to_standard_form() model._convert_to_canonical_form() self.assertEqual( model._canonical_constraints, [([1, -2, -3, -2, 1, 0, 1, 0], 3), ([1, -1, 2, 1, 0, -1, 0, 1], 11)], ) self.assertEqual( model._canonical_cost_function, ([-2, 3, -1, -1, 0, 0, 0, 0], -10) ) self.assertEqual( model._canonical_artificial_function, ([-2, 3, 1, 1, -1, 1, 0, 0], -14) ) def test__build_tableau_canonical_form(self): """Test Model._build_tableau_canonical_form method.""" model = lp.Model(["x1", "x2", "x3", "x4"]) model.add_constraint([1, -2, -3, -2], lp.EQ, 3) model.add_constraint([1, -1, 2, 1], lp.EQ, 11) model.add_cost_function(lp.MINIMIZE, [2, -3, 1, 1], 0) model._convert_to_standard_form() model._convert_to_canonical_form() tableau = model._build_tableau_canonical_form() self.assertEqual(tableau._basic_variables, [4, 5]) self.assertEqual( tableau._tableau, [ [1, -2, -3, -2, 1, 0, 3], [1, -1, 2, 1, 0, 1, 11], [2, -3, 1, 1, 0, 0, 0], [-2, 3, 1, 1, 0, 0, -14], ], ) def test___str__(self): """Test Model.__str__ method.""" model = lp.Model(["x1", "x2", "x3", "x4", "x5"]) model.add_constraint([-6, 0, 1, -2, 2], lp.LTE, 6) model.add_constraint([-3, 1, 0, 6, 3], lp.EQ, 15) model.add_cost_function(lp.MINIMIZE, [5, 0, 0, 3, -2], -21) self.assertEqual( model.__str__(), """Minimize: 5 x1 + 0 x2 + 0 x3 + 3 x4 - 2 x5 - 21 Subject to: -6 x1 + 0 x2 + 1 x3 - 2 x4 + 2 x5 <= 6 -3 x1 + 1 x2 + 0 x3 + 6 x4 + 3 x5 = 15 x1, x2, x3, x4, x5 >= 0""", ) class TableauTestCase(unittest.TestCase): def test_add_constraint_fails(self): """Test Tableau.add_constraint asserts.""" tableau = lp.Tableau(3, 2) self.assertRaises(AssertionError, tableau.add_constraint, [1], 0) self.assertRaises(AssertionError, tableau.add_constraint, [1, 2, 3, 4, 5], 0) tableau.add_constraint([1, 2, 3, 4], 0) self.assertRaises(AssertionError, tableau.add_constraint, [1, 2, 3, 4], 0) def test_add_constraint(self): """Test Tableau.add_constraint success.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) self.assertEqual(tableau._basic_variables, [0]) self.assertEqual(tableau._tableau, [[1, 2, 3, 4]]) def test_add_cost_function_fails(self): """Test Tableau.add_cost_function asserts.""" tableau = lp.Tableau(3, 2) self.assertRaises(AssertionError, tableau.add_cost_function, [1]) self.assertRaises(AssertionError, tableau.add_cost_function, [1, 2, 3, 4]) def test_add_cost_function(self): """Test Tableau.add_cost_function success.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) self.assertEqual(tableau._tableau, [[1, 2, 3, 4], [0, 1, 2, 3], [0, 0, 1, 2]]) def test_add_artificial_function_fails(self): """Test Tableau.add_artificial_function asserts.""" tableau = lp.Tableau(3, 2) self.assertRaises(AssertionError, tableau.add_artificial_function, [1]) self.assertRaises(AssertionError, tableau.add_artificial_function, [1, 2, 3, 4]) def test_add_artificial_function(self): """Test Tableau.add_artificial_function success.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) tableau.add_artificial_function([1, 3, 5, 7]) self.assertTrue(tableau._artificial) self.assertEqual( tableau._tableau, [[1, 2, 3, 4], [0, 1, 2, 3], [0, 0, 1, 2], [1, 3, 5, 7]] ) def test_constraints(self): """Test Tableau.constraints method.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) tableau.add_artificial_function([1, 3, 5, 7]) self.assertEqual(tableau.constraints(), [[1, 2, 3, 4], [0, 1, 2, 3]]) def test_cost_function(self): """Test Tableau.cost_function for non artificial models.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) self.assertEqual(tableau.cost_function(), [0, 0, 1, 2]) def test_cost_function_artificial(self): """Test Tableau.cost_function for artificial models.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) tableau.add_artificial_function([1, 3, 5, 7]) self.assertEqual(tableau.cost_function(), [1, 3, 5, 7]) def test_drop_artificial_not_minimal(self): """Test Tableau.drop_artificial fails when not minimal.""" tableau = lp.Tableau(4, 2) tableau.add_constraint([1, 2, 1, 0, 5], 0) tableau.add_constraint([0, 1, 0, 1, 5], 1) tableau.add_cost_function([2, 3, 0, 0, 5]) tableau.add_artificial_function([-1, -3, 0, 0, -15]) self.assertRaises(AssertionError, tableau.drop_artificial) def test_drop_artificial_artificial_variable(self): """Test Tableau.drop_artificial fails when artificial variable.""" tableau = lp.Tableau(4, 2) tableau.add_constraint([1, 2, 1, 0, 5], 2) tableau.add_constraint([0, 1, 0, 1, 5], 3) tableau.add_cost_function([2, 3, 0, 0, 5]) tableau.add_artificial_function([1, 3, 0, 0, -15]) self.assertRaises(AssertionError, tableau.drop_artificial) def test_drop_artificial(self): """Test Tableau.drop_artificial method.""" tableau = lp.Tableau(4, 2) tableau.add_constraint([1, 2, 1, 0, 5], 0) tableau.add_constraint([0, 1, 0, 1, 5], 1) tableau.add_cost_function([2, 3, 0, 0, 5]) tableau.add_artificial_function([1, 3, 0, 0, -15]) tableau.drop_artificial() self.assertFalse(tableau._artificial) self.assertEqual(tableau._tableau, [[1, 2, 5], [0, 1, 5], [2, 3, 5]]) def test_simplex(self): """Test Tableau.simplex method.""" tableau = lp.Tableau(5, 2) tableau.add_constraint([-6, 0, 1, -2, 2, 6], 2) tableau.add_constraint([-3, 1, 0, 6, 3, 15], 1) tableau.add_cost_function([5, 0, 0, 3, -2, -21]) tableau.simplex() self.assertEqual(tableau._basic_variables, [4, 0]) self.assertEqual( tableau._tableau, [ [0.0, 1 / 2, -1 / 4, 7 / 2, 1.0, 6.0], [1.0, 1 / 6, -1 / 4, 3 / 2, 0.0, 1.0], [0.0, 1 / 6, 3 / 4, 5 / 2, 0.0, -14.0], ], ) def test_is_canonical_not_canonical(self): """Test Tableau.is_canonical when not canonical.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 0, 5], 1) tableau.add_constraint([0, 1, 0, 5], 2) tableau.add_cost_function([2, 3, 0, 5]) self.assertFalse(tableau.is_canonical()) def test_is_canonical_almost_canonical(self): """Test Tableau.is_canonical when no canonical.""" tableau = lp.Tableau(2, 2) tableau.add_constraint([1, 2, 5], 0) tableau.add_constraint([0, 1, 5], 1) tableau.add_cost_function([2, 3, 5]) self.assertFalse(tableau.is_canonical()) def test_is_canonical(self): """Test Tableau.is_canonical when canonical.""" tableau = lp.Tableau(2, 2) tableau.add_constraint([1, 0, 5], 0) tableau.add_constraint([0, 1, 5], 1) tableau.add_cost_function([0, 0, 5]) self.assertTrue(tableau.is_canonical()) def test_is_minimum_not_minimum(self): """Test Tableau.is_minimum method.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, -1, 2]) self.assertFalse(tableau.is_minimum()) def test_is_minimum_artificial_not_minimum(self): """Test Tableau.is_minimum method.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) tableau.add_artificial_function([2, -3, 0, 0]) self.assertFalse(tableau.is_minimum()) def test_is_minimum(self): """Test Tableau.is_minimum method.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, 1, 2]) self.assertTrue(tableau.is_minimum()) def test_is_minimum_artificial(self): """Test Tableau.is_minimum method.""" tableau = lp.Tableau(3, 2) tableau.add_constraint([1, 2, 3, 4], 0) tableau.add_constraint([0, 1, 2, 3], 1) tableau.add_cost_function([0, 0, -1, 2]) tableau.add_artificial_function([2, 3, 0, 0]) self.assertTrue(tableau.is_minimum()) def test_is_basic_feasible_solution_fails(self): """Test Tableau.is_basic_feasible_solution failures.""" tableau = lp.Tableau(4, 2) tableau.add_constraint([1, 1, 2, 1, 6], 0) tableau.add_constraint([0, 3, 1, 8, 3], 1) tableau.add_cost_function([0, 0, 0, 0, 0]) self.assertRaises(AssertionError, tableau.is_basic_feasible_solution) def test_is_basic_feasible_solution_non_existent(self): """Test Tableau.is_basic_feasible_solution method.""" tableau = lp.Tableau(4, 2) tableau.add_constraint([1, 0, 1.667, 1.667, 5], 0) tableau.add_constraint([0, 1, 0.333, 2.667, -1], 1) tableau.add_cost_function([0, 0, 0, 0, 0]) self.assertFalse(tableau.is_basic_feasible_solution()) def test_is_basic_feasible_solution(self): """Test Tableau.is_basic_feasible_solution method.""" tableau = lp.Tableau(4, 2) tableau.add_constraint([1, 0, 1.667, 1.667, 5], 0) tableau.add_constraint([0, 1, 0.333, 2.667, 1], 1) tableau.add_cost_function([0, 0, 0, 0, 0]) self.assertTrue(tableau.is_basic_feasible_solution()) def test_is_bound(self): """Test Tableau.is_bound method.""" pass def test__get_pivoting_column(self): """Test Tableau._get_pivoting_column method.""" tableau = lp.Tableau(5, 2) tableau.add_constraint([-6, 0, 1, -2, 2, 6], 2) tableau.add_constraint([-3, 1, 0, 6, 3, 15], 1) tableau.add_cost_function([5, 0, 0, 3, -2, -21]) self.assertEqual(tableau._get_pivoting_column(), 4) def test__get_pivoting_row(self): """Test Tableau._get_pivoting_row method.""" tableau = lp.Tableau(5, 2) tableau.add_constraint([-6, 0, 1, -2, 2, 6], 2) tableau.add_constraint([-3, 1, 0, 6, 3, 15], 1) tableau.add_cost_function([5, 0, 0, 3, -2, -21]) self.assertEqual(tableau._get_pivoting_row(4), 0) def test__pivote(self): """Test Tableau._pivote method.""" tableau = lp.Tableau(3, 3) tableau.add_constraint([1, 4, 2, 6], 0) tableau.add_constraint([3, 14, 8, 16], 1) tableau.add_constraint([4, 21, 10, 28], 2) # Pivote by x1 in the first equation tableau._pivote(0, 0) self.assertEqual( tableau._tableau, [[1.0, 4.0, 2.0, 6.0], [0.0, 2.0, 2.0, -2.0], [0.0, 5.0, 2.0, 4.0]], ) # Pivote by x2 in the second equation tableau._pivote(1, 1) self.assertEqual( tableau._tableau, [[1.0, 0.0, -2.0, 10.0], [0.0, 1.0, 1.0, -1.0], [0.0, 0.0, -3.0, 9.0]], ) # Pivote by x3 in the third equation tableau._pivote(2, 2) self.assertEqual( tableau._tableau, [[1.0, 0.0, 0.0, 4.0], [0.0, 1.0, 0.0, 2.0], [0.0, 0.0, 1.0, -3.0]], ) if __name__ == "__main__": unittest.main() 0707010000008A000081A40000000000000000000000016130D1CF000038D0000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_partitioned.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import unittest from unittest.mock import patch from states import partitioned class PartitionedTestCase(unittest.TestCase): @patch("states.partitioned.__salt__") def test_check_label(self, __salt__): fdisk_output = """Error: /dev/sda: unrecognised disk label BYT; /dev/sda:25.8GB:scsi:512:512:unknown:ATA QEMU HARDDISK:; Error: /dev/sdb: unrecognised disk label BYT; /dev/sdb:25.8GB:scsi:512:512:unknown:ATA QEMU HARDDISK:; """ __salt__.__getitem__.return_value = lambda _: fdisk_output self.assertFalse(partitioned._check_label("/dev/sda", "msdos")) self.assertFalse(partitioned._check_label("/dev/sda", "dos")) self.assertFalse(partitioned._check_label("/dev/sda", "gpt")) fdisk_output = """BYT; /dev/sda:25.8GB:scsi:512:512:msdos:ATA QEMU HARDDISK:; Error: /dev/sdb: unrecognised disk label BYT; /dev/sdb:25.8GB:scsi:512:512:unknown:ATA QEMU HARDDISK:; """ __salt__.__getitem__.return_value = lambda _: fdisk_output self.assertTrue(partitioned._check_label("/dev/sda", "msdos")) self.assertTrue(partitioned._check_label("/dev/sda", "dos")) self.assertFalse(partitioned._check_label("/dev/sda", "gpt")) fdisk_output = """BYT; /dev/sda:500GB:scsi:512:512:gpt:ATA ST3500413AS:pmbr_boot; 1:1049kB:9437kB:8389kB:::bios_grub; 2:9437kB:498GB:498GB:btrfs::legacy_boot; 3:498GB:500GB:2147MB:linux-swap(v1)::swap; BYT; /dev/sdb:2000GB:scsi:512:4096:msdos:ATA ST2000DM001-1CH1:; 1:1049kB:2000GB:2000GB:ext4::type=83; """ __salt__.__getitem__.return_value = lambda _: fdisk_output self.assertFalse(partitioned._check_label("/dev/sda", "msdos")) self.assertFalse(partitioned._check_label("/dev/sda", "dos")) self.assertTrue(partitioned._check_label("/dev/sda", "gpt")) @patch("states.partitioned.__opts__") @patch("states.partitioned.__salt__") def test_labeled(self, __salt__, __opts__): __opts__.__getitem__.return_value = False __salt__.__getitem__.return_value = lambda _: "/dev/sda:msdos:" self.assertEqual( partitioned.labeled("/dev/sda", "msdos"), { "name": "/dev/sda", "result": True, "changes": {}, "comment": ["Label already set to msdos"], }, ) __salt__.__getitem__.side_effect = ( lambda _: "", lambda _a, _b: True, lambda _: "/dev/sda:msdos:", ) self.assertEqual( partitioned.labeled("/dev/sda", "msdos"), { "name": "/dev/sda", "result": True, "changes": {"label": "Label set to msdos in /dev/sda"}, "comment": ["Label set to msdos in /dev/sda"], }, ) @patch("states.partitioned.__salt__") def test_get_partition_type(self, __salt__): __salt__.__getitem__.return_value = ( lambda _: """ Model: ATA ST2000DM001-9YN1 (scsi) Disk /dev/sda: 2000GB Sector size (logical/physical): 512B/4096B Partition Table: msdos Disk Flags: Number Start End Size Type File system Flags 1 1049kB 2155MB 2154MB primary linux-swap(v1) type=82 2 2155MB 45.1GB 43.0GB primary btrfs boot, type=83 3 45.1GB 2000GB 1955GB primary xfs type=83 """ ) self.assertEqual( partitioned._get_partition_type("/dev/sda"), {"1": "primary", "2": "primary", "3": "primary"}, ) __salt__.__getitem__.return_value = ( lambda _: """ Model: ATA QEMU HARDDISK (scsi) Disk /dev/sda: 25.8GB Sector size (logical/physical): 512B/512B Partition Table: msdos Disk Flags: Number Start End Size Type File system Flags 1 1049kB 11.5MB 10.5MB extended type=05 5 2097kB 5243kB 3146kB logical type=83 3 11.5GB 22.0MB 10.5MB primary type=83 """ ) self.assertEqual( partitioned._get_partition_type("/dev/sda"), {"1": "extended", "5": "logical", "3": "primary"}, ) @patch("states.partitioned.__salt__") def test_get_cached_partitions(self, __salt__): __salt__.__getitem__.side_effect = [ lambda _: "1 extended", lambda _, unit: {"info": None, "partitions": {"1": {}}}, ] self.assertEqual( partitioned._get_cached_partitions("/dev/sda", "s"), {"1": {"type": "extended"}}, ) partitioned._invalidate_cached_partitions() __salt__.__getitem__.side_effect = [ lambda _: "", lambda _, unit: {"info": None, "partitions": {"1": {}}}, ] self.assertEqual( partitioned._get_cached_partitions("/dev/sda", "s"), {"1": {"type": "primary"}}, ) @patch("states.partitioned._get_cached_partitions") def test_check_partition(self, _get_cached_partitions): _get_cached_partitions.return_value = { "1": {"type": "primary", "size": "10s", "start": "0s", "end": "10s"} } self.assertTrue( partitioned._check_partition("/dev/sda", 1, "primary", "0s", "10s") ) self.assertTrue( partitioned._check_partition("/dev/sda", "1", "primary", "0s", "10s") ) self.assertFalse( partitioned._check_partition("/dev/sda", "1", "primary", "10s", "20s") ) self.assertEqual( partitioned._check_partition("/dev/sda", "2", "primary", "10s", "20s"), None ) _get_cached_partitions.return_value = { "1": {"type": "primary", "size": "100kB", "start": "0.5kB", "end": "100kB"} } self.assertTrue( partitioned._check_partition("/dev/sda", "1", "primary", "0kB", "100kB") ) self.assertTrue( partitioned._check_partition("/dev/sda", "1", "primary", "1kB", "100kB") ) self.assertFalse( partitioned._check_partition("/dev/sda", "1", "primary", "1.5kB", "100kB") ) @patch("states.partitioned._get_cached_partitions") def test_get_first_overlapping_partition(self, _get_cached_partitions): _get_cached_partitions.return_value = {} self.assertEqual( partitioned._get_first_overlapping_partition("/dev/sda", "0s"), None ) _get_cached_partitions.return_value = { "1": { "number": "1", "type": "primary", "size": "10s", "start": "0s", "end": "10s", } } self.assertEqual( partitioned._get_first_overlapping_partition("/dev/sda", "0s"), "1" ) _get_cached_partitions.return_value = { "1": { "number": "1", "type": "primary", "size": "100kB", "start": "0.51kB", "end": "100kB", } } self.assertEqual( partitioned._get_first_overlapping_partition("/dev/sda", "0kB"), "1" ) _get_cached_partitions.return_value = { "1": { "number": "1", "type": "extended", "size": "10s", "start": "0s", "end": "10s", }, "5": { "number": "5", "type": "logical", "size": "4s", "start": "1s", "end": "5s", }, } self.assertEqual( partitioned._get_first_overlapping_partition("/dev/sda", "0s"), "1" ) self.assertEqual( partitioned._get_first_overlapping_partition("/dev/sda", "1s"), "5" ) @patch("states.partitioned._get_cached_info") @patch("states.partitioned._get_cached_partitions") def test_get_partition_number_primary( self, _get_cached_partitions, _get_cached_info ): _get_cached_info.return_value = {"partition table": "msdos"} _get_cached_partitions.return_value = {} partition_data = ("/dev/sda", "primary", "0s", "10s") self.assertEqual(partitioned._get_partition_number(*partition_data), "1") _get_cached_partitions.return_value = { "1": { "number": "1", "type": "primary", "size": "10s", "start": "0s", "end": "10s", } } self.assertEqual(partitioned._get_partition_number(*partition_data), "1") partition_data = ("/dev/sda", "primary", "0s", "10s") self.assertEqual(partitioned._get_partition_number(*partition_data), "1") _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, "2": {"number": "2", "type": "primary", "start": "11s", "end": "20s"}, "3": {"number": "3", "type": "primary", "start": "21s", "end": "30s"}, "4": {"number": "4", "type": "primary", "start": "31s", "end": "40s"}, } partition_data = ("/dev/sda", "primary", "41s", "50s") self.assertRaises( partitioned.EnumerateException, partitioned._get_partition_number, *partition_data ) _get_cached_info.return_value = {"partition table": "gpt"} partition_data = ("/dev/sda", "primary", "41s", "50s") self.assertEqual(partitioned._get_partition_number(*partition_data), "5") @patch("states.partitioned._get_cached_info") @patch("states.partitioned._get_cached_partitions") def test_get_partition_number_extended( self, _get_cached_partitions, _get_cached_info ): _get_cached_info.return_value = {"partition table": "msdos"} _get_cached_partitions.return_value = {} partition_data = ("/dev/sda", "extended", "0s", "10s") self.assertEqual(partitioned._get_partition_number(*partition_data), "1") _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, } partition_data = ("/dev/sda", "extended", "21s", "30s") self.assertEqual(partitioned._get_partition_number(*partition_data), "2") _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, "2": {"number": "2", "type": "extended", "start": "11s", "end": "20s"}, } self.assertRaises( partitioned.EnumerateException, partitioned._get_partition_number, *partition_data ) _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, "2": {"number": "2", "type": "primary", "start": "11s", "end": "20s"}, "3": {"number": "3", "type": "primary", "start": "21s", "end": "30s"}, "4": {"number": "4", "type": "primary", "start": "31s", "end": "40s"}, } partition_data = ("/dev/sda", "extended", "41s", "50s") self.assertRaises( partitioned.EnumerateException, partitioned._get_partition_number, *partition_data ) _get_cached_info.return_value = {"partition table": "gpt"} _get_cached_partitions.return_value = {} self.assertRaises( partitioned.EnumerateException, partitioned._get_partition_number, *partition_data ) @patch("states.partitioned._get_cached_info") @patch("states.partitioned._get_cached_partitions") def test_get_partition_number_logial( self, _get_cached_partitions, _get_cached_info ): _get_cached_info.return_value = {"partition table": "msdos"} _get_cached_partitions.return_value = {} partition_data = ("/dev/sda", "logical", "0s", "10s") self.assertRaises( partitioned.EnumerateException, partitioned._get_partition_number, *partition_data ) _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, } partition_data = ("/dev/sda", "logical", "12s", "15s") self.assertRaises( partitioned.EnumerateException, partitioned._get_partition_number, *partition_data ) _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, "2": {"number": "2", "type": "extended", "start": "11s", "end": "20s"}, } self.assertEqual(partitioned._get_partition_number(*partition_data), "5") _get_cached_partitions.return_value = { "1": {"number": "1", "type": "primary", "start": "0s", "end": "10s"}, "2": {"number": "2", "type": "extended", "start": "11s", "end": "20s"}, "5": {"number": "5", "type": "logical", "start": "12s", "end": "15s"}, } self.assertEqual(partitioned._get_partition_number(*partition_data), "5") partition_data = ("/dev/sda", "logical", "16s", "19s") self.assertEqual(partitioned._get_partition_number(*partition_data), "6") @patch("states.partitioned._get_partition_number") @patch("states.partitioned.__salt__") def test_mkparted(self, __salt__, _get_partition_number): pass if __name__ == "__main__": unittest.main() 0707010000008B000081A40000000000000000000000016130D1CF00006162000000000000000000000000000000000000003800000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_partmod.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import unittest from unittest.mock import patch from salt.exceptions import SaltInvocationError from disk import ParseException from modules import partmod from modules import filters class PartmodTestCase(unittest.TestCase): @patch("modules.partmod.__grains__") def test_prepare_partition_data_fails_fs_type(self, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "error"}], }, }, } __grains__.__getitem__.return_value = False with self.assertRaises(SaltInvocationError) as cm: partmod.prepare_partition_data(partitions) self.assertTrue("type error not recognized" in str(cm.exception)) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_fails_units_invalid(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "1Kilo", "type": "swap"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid with self.assertRaises(ParseException) as cm: partmod.prepare_partition_data(partitions) self.assertTrue("Kilo not recognized" in str(cm.exception)) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_fails_units_initial_gap(self, __salt__, __grains__): partitions = { "config": {"initial_gap": "1024kB"}, "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "1MB", "type": "swap"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid with self.assertRaises(SaltInvocationError) as cm: partmod.prepare_partition_data(partitions) self.assertTrue("Units needs to be" in str(cm.exception)) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_no_gap(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_msdos_no_gap(self, __salt__, __grains__): partitions = { "config": {"label": "msdos"}, "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_local_msdos_no_gap(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "label": "msdos", "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_gpt_no_gap(self, __salt__, __grains__): partitions = { "config": {"label": "gpt"}, "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "gpt", "pmbr_boot": True, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_local_gpt_no_gap(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "label": "gpt", "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "gpt", "pmbr_boot": True, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_gap(self, __salt__, __grains__): partitions = { "config": {"initial_gap": "1MB"}, "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "1.0MB", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_local_gap(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "initial_gap": "1MB", "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "1.0MB", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_fails_rest(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [ {"number": 1, "size": "rest", "type": "swap"}, {"number": 2, "size": "rest", "type": "linux"}, ], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid with self.assertRaises(SaltInvocationError) as cm: partmod.prepare_partition_data(partitions) self.assertTrue("rest free space" in str(cm.exception)) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_fails_units(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [ {"number": 1, "size": "1%", "type": "swap"}, {"number": 2, "size": "2MB", "type": "linux"}, ], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid with self.assertRaises(SaltInvocationError) as cm: partmod.prepare_partition_data(partitions) self.assertTrue("Units needs to be" in str(cm.exception)) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_efi_partitions(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "label": "gpt", "partitions": [ {"number": 1, "size": "500MB", "type": "efi"}, {"number": 2, "size": "10000MB", "type": "linux"}, {"number": 3, "size": "5000MB", "type": "swap"}, ], }, }, } __grains__.__getitem__.return_value = True __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "gpt", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "fat16", "flags": ["esp"], "start": "0MB", "end": "500.0MB", }, { "part_id": "/dev/sda2", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "500.0MB", "end": "10500.0MB", }, { "part_id": "/dev/sda3", "part_type": "primary", "fs_type": "linux-swap", "flags": None, "start": "10500.0MB", "end": "15500.0MB", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_muti_label(self, __salt__, __grains__): partitions = { "config": {"label": "msdos"}, "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, "/dev/sdb": { "label": "gpt", "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, "/dev/sdb": { "label": "gpt", "pmbr_boot": True, "partitions": [ { "part_id": "/dev/sdb1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_multi_gap(self, __salt__, __grains__): partitions = { "config": {"initial_gap": "1MB"}, "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, "/dev/sdb": { "initial_gap": "2MB", "partitions": [{"number": 1, "size": "20MB", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "1.0MB", "end": "100%", }, ], }, "/dev/sdb": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sdb1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "2.0MB", "end": "22.0MB", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_lvm(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "lvm"}], }, "/dev/sdb": { "partitions": [{"number": 1, "size": "rest", "type": "lvm"}], }, "/dev/sdc": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": ["lvm"], "start": "0%", "end": "100%", }, ], }, "/dev/sdb": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sdb1", "part_type": "primary", "fs_type": "ext2", "flags": ["lvm"], "start": "0%", "end": "100%", }, ], }, "/dev/sdc": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sdc1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_raid(self, __salt__, __grains__): partitions = { "devices": { "/dev/sda": { "partitions": [{"number": 1, "size": "rest", "type": "raid"}], }, "/dev/sdb": { "partitions": [{"number": 1, "size": "rest", "type": "raid"}], }, "/dev/sdc": { "partitions": [{"number": 1, "size": "rest", "type": "linux"}], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/sda": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sda1", "part_type": "primary", "fs_type": "ext2", "flags": ["raid"], "start": "0%", "end": "100%", }, ], }, "/dev/sdb": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sdb1", "part_type": "primary", "fs_type": "ext2", "flags": ["raid"], "start": "0%", "end": "100%", }, ], }, "/dev/sdc": { "label": "msdos", "pmbr_boot": False, "partitions": [ { "part_id": "/dev/sdc1", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "0%", "end": "100%", }, ], }, }, ) @patch("modules.partmod.__grains__") @patch("modules.partmod.__salt__") def test_prepare_partition_data_bios_gpt_post_raid(self, __salt__, __grains__): partitions = { "devices": { "/dev/md0": { "label": "gpt", "partitions": [ {"number": 1, "size": "8MB", "type": "boot"}, {"number": 2, "size": "rest", "type": "linux"}, ], }, }, } __grains__.__getitem__.return_value = False __salt__.__getitem__.return_value = filters.is_raid self.assertEqual( partmod.prepare_partition_data(partitions), { "/dev/md0": { "label": "gpt", "pmbr_boot": True, "partitions": [ { "part_id": "/dev/md0p1", "part_type": "primary", "fs_type": "ext2", "flags": ["bios_grub"], "start": "0MB", "end": "8.0MB", }, { "part_id": "/dev/md0p2", "part_type": "primary", "fs_type": "ext2", "flags": None, "start": "8.0MB", "end": "100%", }, ], }, }, ) if __name__ == "__main__": unittest.main() 0707010000008C000081A40000000000000000000000016130D1CF000040D8000000000000000000000000000000000000004200000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_state_suseconnect.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations import unittest from unittest.mock import patch, MagicMock from states import suseconnect from salt.exceptions import CommandExecutionError class SUSEConnectTestCase(unittest.TestCase): def test__status_registered(self): salt_mock = { "suseconnect.status": MagicMock( return_value=[ { "identifier": "SLES", "version": "15.2", "arch": "x86_64", "status": "Registered", "subscription_status": "ACTIVE", }, { "identifier": "sle-module-basesystem", "version": "15.2", "arch": "x86_64", "status": "Registered", }, { "identifier": "sle-module-server-applications", "version": "15.2", "arch": "x86_64", "status": "Registered", }, ] ), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect._status(None), ( [ "SLES/15.2/x86_64", "sle-module-basesystem/15.2/x86_64", "sle-module-server-applications/15.2/x86_64", ], ["SLES/15.2/x86_64"], ), ) def test__status_unregistered(self): salt_mock = { "suseconnect.status": MagicMock( return_value=[ { "identifier": "openSUSE", "version": "20191014", "arch": "x86_64", "status": "Not Registered", }, ] ), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual(suseconnect._status(None), ([], [])) @patch("states.suseconnect._status") def test__is_registered_default_product(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) self.assertTrue(suseconnect._is_registered(product=None, root=None)) @patch("states.suseconnect._status") def test__is_registered_product(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) self.assertTrue( suseconnect._is_registered(product="SLES/15.2/x86_64", root=None) ) @patch("states.suseconnect._status") def test__is_registered_default_product_unregistered(self, _status): _status.return_value = ([], []) self.assertFalse(suseconnect._is_registered(product=None, root=None)) @patch("states.suseconnect._status") def test__is_registered_product_unregistered(self, _status): _status.return_value = ([], []) self.assertFalse( suseconnect._is_registered(product="SLES/15.2/x86_64", root=None) ) @patch("states.suseconnect._status") def test__is_registered_other_product_unregistered(self, _status): _status.return_value = ([], ["SLES/15.2/x86_64"]) self.assertFalse( suseconnect._is_registered(product="openSUSE/15.2/x86_64", root=None) ) @patch("states.suseconnect._status") def test_registered_default_product(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) result = suseconnect.registered("my_setup", "regcode") self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {}, "comment": ["Product or module default already registered"], }, ) @patch("states.suseconnect._status") def test_registered_named_product(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) result = suseconnect.registered("SLES/15.2/x86_64", "regcode") self.assertEqual( result, { "name": "SLES/15.2/x86_64", "result": True, "changes": {}, "comment": ["Product or module SLES/15.2/x86_64 already registered"], }, ) @patch("states.suseconnect._status") def test_registered_product(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) result = suseconnect.registered( "my_setup", "regcode", product="SLES/15.2/x86_64" ) self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {}, "comment": ["Product or module SLES/15.2/x86_64 already registered"], }, ) @patch("states.suseconnect._status") def test_registered_test(self, _status): _status.return_value = ([], []) opts_mock = {"test": True} with patch.dict(suseconnect.__opts__, opts_mock): result = suseconnect.registered("my_setup", "regcode") self.assertEqual( result, { "name": "my_setup", "result": None, "changes": {"default": True}, "comment": ["Product or module default would be registered"], }, ) @patch("states.suseconnect._status") def test_registered_fail_register(self, _status): _status.return_value = ([], []) opts_mock = {"test": False} salt_mock = { "suseconnect.register": MagicMock( side_effect=CommandExecutionError("some error") ) } with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.registered("my_setup", "regcode") self.assertEqual( result, { "name": "my_setup", "result": False, "changes": {}, "comment": ["Error registering default: some error"], }, ) @patch("states.suseconnect._status") def test_registered_fail_register_end(self, _status): _status.return_value = ([], []) opts_mock = {"test": False} salt_mock = {"suseconnect.register": MagicMock()} with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.registered("my_setup", "regcode") self.assertEqual( result, { "name": "my_setup", "result": False, "changes": {"default": True}, "comment": ["Product or module default failed to register"], }, ) @patch("states.suseconnect._status") def test_registered_succeed_register(self, _status): _status.side_effect = [ ([], []), (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]), ] opts_mock = {"test": False} salt_mock = {"suseconnect.register": MagicMock()} with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.registered("my_setup", "regcode") self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {"default": True}, "comment": ["Product or module default registered"], }, ) salt_mock["suseconnect.register"].assert_called_with( "regcode", product=None, email=None, url=None, root=None ) @patch("states.suseconnect._status") def test_registered_succeed_register_params(self, _status): _status.side_effect = [ ([], []), (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]), ] opts_mock = {"test": False} salt_mock = {"suseconnect.register": MagicMock()} with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.registered( "my_setup", "regcode", product="SLES/15.2/x86_64", email="user@example.com", url=None, root=None, ) self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {"SLES/15.2/x86_64": True}, "comment": ["Product or module SLES/15.2/x86_64 registered"], }, ) salt_mock["suseconnect.register"].assert_called_with( "regcode", product="SLES/15.2/x86_64", email="user@example.com", url=None, root=None, ) @patch("states.suseconnect._status") def test_deregistered_default_product(self, _status): _status.return_value = ([], []) result = suseconnect.deregistered("my_setup") self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {}, "comment": ["Product or module default already deregistered"], }, ) @patch("states.suseconnect._status") def test_deregistered_named_product(self, _status): _status.return_value = ([], []) result = suseconnect.deregistered("SLES/15.2/x86_64") self.assertEqual( result, { "name": "SLES/15.2/x86_64", "result": True, "changes": {}, "comment": [ "Product or module SLES/15.2/x86_64 already deregistered" ], }, ) @patch("states.suseconnect._status") def test_deregistered_other_named_product(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) result = suseconnect.deregistered("openSUSE/15.2/x86_64") self.assertEqual( result, { "name": "openSUSE/15.2/x86_64", "result": True, "changes": {}, "comment": [ "Product or module openSUSE/15.2/x86_64 already deregistered" ], }, ) @patch("states.suseconnect._status") def test_deregistered_product(self, _status): _status.return_value = ([], []) result = suseconnect.deregistered("my_setup", product="SLES/15.2/x86_64") self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {}, "comment": [ "Product or module SLES/15.2/x86_64 already deregistered" ], }, ) @patch("states.suseconnect._status") def test_deregistered_test(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) opts_mock = {"test": True} with patch.dict(suseconnect.__opts__, opts_mock): result = suseconnect.deregistered("my_setup") self.assertEqual( result, { "name": "my_setup", "result": None, "changes": {"default": True}, "comment": ["Product or module default would be deregistered"], }, ) @patch("states.suseconnect._status") def test_deregistered_fail_deregister(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) opts_mock = {"test": False} salt_mock = { "suseconnect.deregister": MagicMock( side_effect=CommandExecutionError("some error") ) } with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.deregistered("my_setup") self.assertEqual( result, { "name": "my_setup", "result": False, "changes": {}, "comment": ["Error deregistering default: some error"], }, ) @patch("states.suseconnect._status") def test_deregistered_fail_deregister_end(self, _status): _status.return_value = (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]) opts_mock = {"test": False} salt_mock = {"suseconnect.deregister": MagicMock()} with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.deregistered("my_setup") self.assertEqual( result, { "name": "my_setup", "result": False, "changes": {"default": True}, "comment": ["Product or module default failed to deregister"], }, ) @patch("states.suseconnect._status") def test_deregistered_succeed_deregister(self, _status): _status.side_effect = [ (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]), ([], []), ] opts_mock = {"test": False} salt_mock = {"suseconnect.deregister": MagicMock()} with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.deregistered("my_setup") self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {"default": True}, "comment": ["Product or module default deregistered"], }, ) salt_mock["suseconnect.deregister"].assert_called_with( product=None, url=None, root=None ) @patch("states.suseconnect._status") def test_deregistered_succeed_register_params(self, _status): _status.side_effect = [ (["SLES/15.2/x86_64"], ["SLES/15.2/x86_64"]), ([], []), ] opts_mock = {"test": False} salt_mock = {"suseconnect.deregister": MagicMock()} with patch.dict(suseconnect.__salt__, salt_mock), patch.dict( suseconnect.__opts__, opts_mock ): result = suseconnect.deregistered( "my_setup", product="SLES/15.2/x86_64", url=None, root=None ) self.assertEqual( result, { "name": "my_setup", "result": True, "changes": {"SLES/15.2/x86_64": True}, "comment": ["Product or module SLES/15.2/x86_64 deregistered"], }, ) salt_mock["suseconnect.deregister"].assert_called_with( product="SLES/15.2/x86_64", url=None, root=None ) 0707010000008D000081A40000000000000000000000016130D1CF00003742000000000000000000000000000000000000003C00000000yomi-0.0.1+git.1630589391.4557cfd/tests/test_suseconnect.py# -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations import os.path import unittest from unittest.mock import patch, MagicMock from salt.exceptions import CommandExecutionError from modules import suseconnect class SUSEConnectTestCase(unittest.TestCase): """ Test cases for salt.modules.suseconnect """ def test_register(self): """ Test suseconnect.register without parameters """ result = {"retcode": 0, "stdout": "Successfully registered system"} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.register("regcode"), "Successfully registered system" ) salt_mock["cmd.run_all"].assert_called_with( ["SUSEConnect", "--regcode", "regcode"] ) def test_register_params(self): """ Test suseconnect.register with parameters """ result = {"retcode": 0, "stdout": "Successfully registered system"} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.register( "regcode", product="sle-ha/15.2/x86_64", email="user@example.com", url="https://scc.suse.com", root="/mnt", ), "Successfully registered system", ) salt_mock["cmd.run_all"].assert_called_with( [ "SUSEConnect", "--regcode", "regcode", "--product", "sle-ha/15.2/x86_64", "--email", "user@example.com", "--url", "https://scc.suse.com", "--root", "/mnt", ] ) def test_register_error(self): """ Test suseconnect.register error """ result = {"retcode": 1, "stdout": "Unknown Registration Code", "stderr": ""} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): suseconnect.register("regcode") def test_deregister(self): """ Test suseconnect.deregister without parameters """ result = {"retcode": 0, "stdout": "Successfully deregistered system"} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.deregister(), "Successfully deregistered system" ) salt_mock["cmd.run_all"].assert_called_with( ["SUSEConnect", "--de-register"] ) def test_deregister_params(self): """ Test suseconnect.deregister with parameters """ result = {"retcode": 0, "stdout": "Successfully deregistered system"} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.deregister( product="sle-ha/15.2/x86_64", url="https://scc.suse.com", root="/mnt", ), "Successfully deregistered system", ) salt_mock["cmd.run_all"].assert_called_with( [ "SUSEConnect", "--de-register", "--product", "sle-ha/15.2/x86_64", "--url", "https://scc.suse.com", "--root", "/mnt", ] ) def test_deregister_error(self): """ Test suseconnect.deregister error """ result = {"retcode": 1, "stdout": "Unknown Product", "stderr": ""} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): suseconnect.deregister() def test_status(self): """ Test suseconnect.status without parameters """ result = { "retcode": 0, "stdout": '[{"identifier":"SLES","version":"15.2",' '"arch":"x86_64","status":"No Registered"}]', } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.status(), [ { "identifier": "SLES", "version": "15.2", "arch": "x86_64", "status": "No Registered", } ], ) salt_mock["cmd.run_all"].assert_called_with(["SUSEConnect", "--status"]) def test_status_params(self): """ Test suseconnect.status with parameters """ result = { "retcode": 0, "stdout": '[{"identifier":"SLES","version":"15.2",' '"arch":"x86_64","status":"No Registered"}]', } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.status(root="/mnt"), [ { "identifier": "SLES", "version": "15.2", "arch": "x86_64", "status": "No Registered", } ], ) salt_mock["cmd.run_all"].assert_called_with( ["SUSEConnect", "--status", "--root", "/mnt"] ) def test_status_error(self): """ Test suseconnect.status error """ result = {"retcode": 1, "stdout": "Some Error", "stderr": ""} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): suseconnect.status() def test__parse_list_extensions(self): """ Test suseconnect.status error """ fixture = os.path.join( os.path.dirname(__file__), "fixtures/list_extensions.txt" ) with open(fixture) as f: self.assertEqual( suseconnect._parse_list_extensions(f.read()), [ "sle-module-basesystem/15.2/x86_64", "sle-module-containers/15.2/x86_64", "sle-module-desktop-applications/15.2/x86_64", "sle-module-development-tools/15.2/x86_64", "sle-we/15.2/x86_64", "sle-module-python2/15.2/x86_64", "sle-module-live-patching/15.2/x86_64", "PackageHub/15.2/x86_64", "sle-module-server-applications/15.2/x86_64", "sle-module-legacy/15.2/x86_64", "sle-module-public-cloud/15.2/x86_64", "sle-ha/15.2/x86_64", "sle-module-web-scripting/15.2/x86_64", "sle-module-transactional-server/15.2/x86_64", ], ) def test_list_extensions(self): """ Test suseconnect.list_extensions without parameters """ result = { "retcode": 0, "stdout": "Activate with: SUSEConnect -p sle-ha/15.2/x86_64", } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual(suseconnect.list_extensions(), ["sle-ha/15.2/x86_64"]) salt_mock["cmd.run_all"].assert_called_with( ["SUSEConnect", "--list-extensions"] ) def test_list_extensions_params(self): """ Test suseconnect.list_extensions with parameters """ result = { "retcode": 0, "stdout": "Activate with: SUSEConnect -p sle-ha/15.2/x86_64", } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.list_extensions(url="https://scc.suse.com", root="/mnt"), ["sle-ha/15.2/x86_64"], ) salt_mock["cmd.run_all"].assert_called_with( [ "SUSEConnect", "--list-extensions", "--url", "https://scc.suse.com", "--root", "/mnt", ] ) def test_list_extensions_error(self): """ Test suseconnect.list_extensions error """ result = { "retcode": 1, "stdout": "To list extensions, you must first register " "the base product", "stderr": "", } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): suseconnect.list_extensions() def test_cleanup(self): """ Test suseconnect.cleanup without parameters """ result = {"retcode": 0, "stdout": "Service has been removed"} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual(suseconnect.cleanup(), "Service has been removed") salt_mock["cmd.run_all"].assert_called_with(["SUSEConnect", "--cleanup"]) def test_cleanup_params(self): """ Test suseconnect.cleanup with parameters """ result = {"retcode": 0, "stdout": "Service has been removed"} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.cleanup(root="/mnt"), "Service has been removed" ) salt_mock["cmd.run_all"].assert_called_with( ["SUSEConnect", "--cleanup", "--root", "/mnt"] ) def test_cleanup_error(self): """ Test suseconnect.cleanup error """ result = {"retcode": 1, "stdout": "some error", "stderr": ""} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): suseconnect.cleanup() def test_rollback(self): """ Test suseconnect.rollback without parameters """ result = { "retcode": 0, "stdout": "Starting to sync system product activations", } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.rollback(), "Starting to sync system product activations" ) salt_mock["cmd.run_all"].assert_called_with(["SUSEConnect", "--rollback"]) def test_rollback_params(self): """ Test suseconnect.rollback with parameters """ result = { "retcode": 0, "stdout": "Starting to sync system product activations", } salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): self.assertEqual( suseconnect.rollback(url="https://scc.suse.com", root="/mnt"), "Starting to sync system product activations", ) salt_mock["cmd.run_all"].assert_called_with( [ "SUSEConnect", "--rollback", "--url", "https://scc.suse.com", "--root", "/mnt", ] ) def test_rollback_error(self): """ Test suseconnect.rollback error """ result = {"retcode": 1, "stdout": "some error", "stderr": ""} salt_mock = { "cmd.run_all": MagicMock(return_value=result), } with patch.dict(suseconnect.__salt__, salt_mock): with self.assertRaises(CommandExecutionError): suseconnect.rollback() 0707010000008E000081ED0000000000000000000000016130D1CF00003A07000000000000000000000000000000000000002F00000000yomi-0.0.1+git.1630589391.4557cfd/yomi-monitor#!/usr/bin/python3 # -*- coding: utf-8 -*- # # Author: Alberto Planas <aplanas@suse.com> # # Copyright 2019 SUSE LLC. # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import argparse import getpass import json import logging import os from pathlib import Path import pprint import ssl import sys import time import urllib.error import urllib.parse import urllib.request LOG = logging.getLogger(__name__) TOKEN_FILE = "~/.salt-api-token" # ANSI color codes BLACK = "\033[1;30m" RED = "\033[1;31m" GREEN = "\033[0;32m" YELLOW = "\033[0;33m" BLUE = "\033[1;34m" MAGENTA = "\033[1;35m" CYAN = "\033[1;36m" WHITE = "\033[1;37m" RESET = "\033[0;0m" class SaltAPI: def __init__( self, url, username, password, eauth, insecure, token_file=TOKEN_FILE, debug=False, ): self.url = url self.username = username self.password = password self.eauth = eauth self.insecure = insecure self.token_file = token_file self.debug = debug is_https = urllib.parse.urlparse(url).scheme == "https" if debug or (is_https and insecure): if insecure: context = ssl._create_unverified_context() handler = urllib.request.HTTPSHandler( context=context, debuglevel=int(debug) ) else: handler = urllib.request.HTTPHandler(debuglevel=int(debug)) opener = urllib.request.build_opener(handler) urllib.request.install_opener(opener) self.token = None self.expire = 0.0 def login(self, remove=False): """Login into the Salt API service.""" if remove: self._drop_token() self.token, self.expire = self._read_token() if self.expire < time.time() + 30: self.token, self.expire = self._login() self._write_token() def logout(self): """Logout from the Salt API service.""" self._drop_token() self._post("/logout") def events(self): """SSE event stream from Salt API service.""" for line in api._req_sse("/events", None, "GET"): line = line.decode("utf-8").strip() if not line or line.startswith((":", "retry:")): continue key, value = line.split(":", 1) if key == "tag": tag = value.strip() continue if key == "data": data = json.loads(value) yield (tag, data) def minions(self, mid=None): """Return the list of minions.""" if mid: action = "/minions/{}".format(mid) else: action = "/minions" return self._get(action)["return"][0] def run_job(self, tgt, fun, **kwargs): """Start an execution command and return jid.""" data = { "tgt": tgt, "fun": fun, } data.update(kwargs) return self._post("/minions", data)["return"][0] def jobs(self, jid=None): """Return the list of jobs.""" if jid: action = "/jobs/{}".format(jid) else: action = "/jobs" return self._get(action)["return"][0] def stats(self): """Return a dump of statistics.""" return self._get("/stats")["return"][0] def _login(self): """Login into the Salt API service.""" data = { "username": self.username, "password": self.password, "eauth": self.eauth, } result = self._post("/login", data) return result["return"][0]["token"], result["return"][0]["expire"] def _get(self, action, data=None): return self._req(action, data, "GET") def _post(self, action, data=None): return self._req(action, data, "POST") def _req(self, action, data, method): """HTTP GET / POST to Salt API.""" headers = { "User-Agent": "salt-autoinstaller monitor", "Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest", } if self.token: headers["X-Auth-Token"] = self.token url = urllib.parse.urljoin(self.url, action) if method == "GET": data = urllib.parse.urlencode(data).encode() if data else None if data: url = "{}?{}".format(url, data) data = None elif method == "POST": data = json.dumps(data).encode() if data else {} else: raise ValueError("Method {} not valid".format(method)) result = {} try: request = urllib.request.Request(url, data, headers) with urllib.request.urlopen(request) as response: result = json.loads(response.read().decode("utf-8")) except (urllib.error.HTTPError, urllib.error.URLError) as exc: LOG.debug("Error with request", exc_info=True) status = getattr(exc, "code", None) if status == 401: print("Authentication denied") if status == 500: print("Server error.") exit(-1) return result def _req_sse(self, action, data, method): """HTTP SSE GET / POST to Salt API.""" headers = { "User-Agent": "salt-autoinstaller monitor", "Accept": "text/event-stream", "Content-Type": "application/json", "Connection": "Keep-Alive", "X-Requested-With": "XMLHttpRequest", } if self.token: headers["X-Auth-Token"] = self.token url = urllib.parse.urljoin(self.url, action) if method == "GET": data = urllib.parse.urlencode(data).encode() if data else None if data: url = "{}?{}".format(url, data) data = None elif method == "POST": data = json.dumps(data).encode() if data else {} else: raise ValueError("Method {} not valid".format(method)) try: request = urllib.request.Request(url, data, headers) with urllib.request.urlopen(request) as response: yield from response except (urllib.error.HTTPError, urllib.error.URLError) as e: LOG.debug("Error with request", exc_info=True) status = getattr(e, "code", None) if status == 401: print("Authentication denied") if status == 500: print("Server error.") exit(-1) def _read_token(self): """Return the token and expire time from the token file.""" token, expire = None, 0.0 if self.token_file: token_path = Path(self.token_file).expanduser() if token_path.is_file(): token, expire = token_path.read_text().split() try: expire = float(expire) except ValueError: expire = 0.0 return token, expire def _write_token(self): """Save the token and expire time into the token file.""" self._drop_token() if self.token_file: token_path = Path(self.token_file).expanduser() token_path.touch(mode=0o600) token_path.write_text("{} {}".format(self.token, self.expire)) def _drop_token(self): """Remove the token file if present.""" if self.token_file: token_path = Path(self.token_file).expanduser() if token_path.is_file(): token_path.unlink() def print_minions(minions): """Print a list of minions.""" print("Registered minions:") for minion in minions: print("- {}".format(minion)) def print_minion(minion): """Print detailed information of a minion.""" pprint.pprint(minion) def print_jobs(jobs): """Print a list of jobs.""" print("Registered jobs:") for job, info in jobs.items(): print("- {}".format(job)) pprint.pprint(info) def print_job(job): """Print detailed information of a job.""" pprint.pprint(job) def print_raw_event(tag, data): """Print raw event without format.""" print("- {}".format(tag)) pprint.pprint(data) def print_yomi_event(tag, data): """Print a Yomi event with format.""" if tag.startswith("yomi/"): id_ = data["data"]["id"] stamp = data["data"]["_stamp"] # Decide the color to represent the node if id_ in print_yomi_event.nodes: color = print_yomi_event.nodes[id_] else: color = print_yomi_event.colors.pop() print_yomi_event.colors.insert(0, color) print_yomi_event.nodes[id_] = color tag = tag.split("/", 1)[1] tag, section = tag.rsplit("/", 1) if section == "enter": print( "[{}{}{}] {} -> [{}STARTING{}] {}".format( color, id_, RESET, stamp, BLUE, RESET, tag ) ) elif section == "success": print( "[{}{}{}] {} -> [{}SUCCESS{}] {}".format( color, id_, RESET, stamp, GREEN, RESET, tag ) ) elif section == "fail": print( "[{}{}{}] {} -> [{}FAIL{}] {}".format( color, id_, RESET, stamp, RED, RESET, tag ) ) # Static-a-like variables to track the rotating color print_yomi_event.nodes = {} print_yomi_event.colors = [RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN] if __name__ == "__main__": parser = argparse.ArgumentParser( description="salt-autoinstaller monitor tool via salt-api." ) parser.add_argument( "-u", "--saltapi-url", default=os.environ.get("SALTAPI_URL", "https://localhost:8000"), help="Specify the host url. Overwrite SALTAPI_URL.", ) parser.add_argument( "-a", "--auth", "--eauth", "--extended-auth", default=os.environ.get("SALTAPI_EAUTH", "pam"), help="Specify the external_auth backend to " "authenticate against and interactively prompt " "for credentials. Overwrite SALTAPI_EAUTH.", ) parser.add_argument( "-n", "--username", default=os.environ.get("SALTAPI_USER"), help="Optional, defaults to user name. will " "be prompt if empty unless --non-interactive. " "Overwrite SALTAPI_USER.", ) parser.add_argument( "-p", "--password", default=os.environ.get("SALTAPI_PASS"), help="Optional, but will be prompted unless " "--non-interactive. Overwrite SALTAPI_PASS.", ) parser.add_argument( "--non-interactive", action="store_true", default=False, help="Optional, fail rather than waiting for input.", ) parser.add_argument( "-r", "--remove", action="store_true", default=False, help="Remove the token cached in the system.", ) parser.add_argument( "-i", "--insecure", action="store_true", default=False, help="Ignore any SSL certificate that may be " "encountered. Note that it is recommended to resolve " "certificate errors for production.", ) parser.add_argument( "-H", "--debug-http", action="store_true", default=False, help=("Output the HTTP request/response headers on " "stderr."), ) parser.add_argument( "-m", "--minions", action="store_true", default=False, help="List available minions.", ) parser.add_argument( "--show-minion", metavar="MID", default=None, help="Show the details of a minion.", ) parser.add_argument( "-j", "--jobs", action="store_true", default=False, help="List available jobs." ) parser.add_argument( "--show-job", metavar="JID", default=None, help="Show the details of a job." ) parser.add_argument( "-e", "--events", action="store_true", default=False, help="Show events from salt-master.", ) parser.add_argument( "-y", "--yomi-events", action="store_true", default=False, help="Show only Yomi events from salt-master.", ) parser.add_argument( "target", nargs="?", help="Minion ID where to launch the installer." ) args = parser.parse_args() if not args.saltapi_url: print("Please, provide a valid Salt API URL", file=sys.stderr) exit(-1) if args.non_interactive: if args.username is None: print("Please, provide a valid user name", file=sys.stderr) exit(-1) if args.password is None: print("Please, provide a valid password", file=sys.stderr) exit(-1) else: if args.username is None: args.username = input("Username: ") if args.password is None: args.password = getpass.getpass(prompt="Password: ") api = SaltAPI( url=args.saltapi_url, username=args.username, password=args.password, eauth=args.auth, insecure=args.insecure, debug=args.debug_http, ) api.login(args.remove) if args.minions: print_minions(api.minions()) if args.show_minion: print_minion(api.minions(args.show_minion)) if args.jobs: print_jobs(api.jobs()) if args.show_job: print_job(api.jobs(args.show_job)) if args.target: print_job(api.run_job(args.target, "state.highstate")) if args.events or args.yomi_events: for tag, data in api.events(): if args.yomi_events: print_yomi_event(tag, data) else: print_raw_event(tag, data) 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!1119 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor