Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
systemsmanagement:Uyuni:Master:ServerContainer
uyuni-tools
uyuni-tools-git-316.5b8e619.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File uyuni-tools-git-316.5b8e619.obscpio of Package uyuni-tools
07070100000000000041FD00000000000000000000000A65DF469000000000000000000000000000000000000000000000000C00000000uyuni-tools07070100000001000041FD00000000000000000000000365DF469000000000000000000000000000000000000000000000001400000000uyuni-tools/.github07070100000002000081B400000000000000000000000165DF46900000036B000000000000000000000000000000000000002D00000000uyuni-tools/.github/PULL_REQUEST_TEMPLATE.md<!-- SPDX-FileCopyrightText: 2024 SUSE LLC SPDX-License-Identifier: Apache-2.0 --> ## What does this PR change? **add description** ## Test coverage - No tests: **add explanation** - No tests: already covered - Unit tests were added - [ ] **DONE** ## Links Issue(s): # - [ ] **DONE** ## Changelogs Make sure the changelogs entries you are adding are compliant with https://github.com/uyuni-project/uyuni/wiki/Contributing#changelogs and https://github.com/uyuni-project/uyuni/wiki/Contributing#uyuni-projectuyuni-repository If you don't need a changelog check, please mark this checkbox: - [ ] No changelog needed If you uncheck the checkbox after the PR is created, you will need to re-run `changelog_test` (see below) # Before you merge Check [How to branch and merge properly](https://github.com/uyuni-project/uyuni/wiki/How-to-branch-and-merge-properly)! 07070100000003000081B400000000000000000000000165DF469000000209000000000000000000000000000000000000002300000000uyuni-tools/.github/dependabot.yml# SPDX-FileCopyrightText: 2024 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" 07070100000004000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001E00000000uyuni-tools/.github/workflows07070100000005000081B400000000000000000000000165DF469000000617000000000000000000000000000000000000002800000000uyuni-tools/.github/workflows/build.yml# SPDX-FileCopyrightText: 2023 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 name: Build on: pull_request: types: - opened - reopened - synchronize release: types: - published jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-tags: true fetch-depth: 0 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v5 with: go-version: '1.20' - name: Install dependencies run: | go get ./... - name: Compute version run: | tag=$(git describe --tags --abbrev=0) version=$(git describe --tags --abbrev=0 | cut -f 3 -d '-') offset=$(git rev-list --count ${tag}..) echo "VERSION=$tag-$offset" >> "$GITHUB_ENV" - name: Build run: | mkdir -p ./bin go build \ -tags netgo \ -ldflags "-X github.com/uyuni-project/uyuni-tools/shared/utils.Version=${{ env.VERSION }}" \ -o ./bin \ ./... - name: Build nok8s run: | mkdir -p ./bin go build \ -tags netgo,nok8s \ -ldflags "-X github.com/uyuni-project/uyuni-tools/shared/utils.Version=${{ env.VERSION }}" \ -o ./bin \ ./... - name: Test with the Go CLI run: go test ./... - name: Upload binaries uses: actions/upload-artifact@v4 with: name: binaries path: ./bin/* 07070100000006000081B400000000000000000000000165DF469000000904000000000000000000000000000000000000002D00000000uyuni-tools/.github/workflows/changelogs.yml# SPDX-FileCopyrightText: 2024 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 name: Changelogs on: push: branches: - main pull_request: types: - opened - reopened - synchronize jobs: changelog_test: name: Test changelog entries runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - id: master name: Get modified master changelog files uses: Ana06/get-changed-files@v2.2.0 with: filter: '*.changes' - name: Fail if the master changelog files are modified if: steps.master.outputs.all run: | echo "Master changelog files cannot be modified directly." echo "Please revert your changes on the following master changelog file(s):" for file in ${{steps.master.outputs.all}} do echo " - $file" done echo echo "See https://github.com/uyuni-project/uyuni/wiki/Contributing for a guide to writing checklogs." exit 1 - id: changelogs name: Get modified changelog files if: "!contains(github.event.pull_request.body, '[x] No changelog needed')" uses: Ana06/get-changed-files@v2.2.0 with: filter: '*.changes.*' - name: Fail if no changelog entries are added if: steps.changelogs.conclusion == 'success' && steps.changelogs.outputs.added_modified == '' run: | echo "No changelog entry found. Please add the required changelog entries." echo "See https://github.com/uyuni-project/uyuni/wiki/Contributing for a guide to writing checklogs." exit 1 # warns the user if they merged the PR, but the changelog test failed warn_user_if_merged: name: Warn user if merged if: always() && github.event.action == 'closed' && github.event.pull_request.merged == true && needs.changelog_test.result == 'failure' needs: changelog_test runs-on: ubuntu-latest steps: - name: Remind the author with a comment uses: peter-evans/create-or-update-comment@v2 with: issue-number: ${{ github.event.pull_request.number }} body: | :warning: No changelog entry has been added. @${{ github.event.pull_request.user.login }}, please add necessary changelog entries with an additional PR. 07070100000007000081B400000000000000000000000165DF469000000810000000000000000000000000000000000000003000000000uyuni-tools/.github/workflows/golangci-lint.yml# SPDX-FileCopyrightText: 2024 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 name: golangci-lint on: push: branches: - main pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. # pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: go-version: '1.20' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: # Require: The version of golangci-lint to use. # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. version: v1.54 # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: golangci-lint command line arguments. # # Note: By default, the `.golangci.yml` file should be at the root of the repository. # The location of the configuration file can be changed by using `--config=` # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Optional: if set to true, then all caching functionality will be completely disabled, # takes precedence over all other caching options. # skip-cache: true # Optional: if set to true, then the action won't cache or restore ~/go/pkg. # skip-pkg-cache: true # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. # skip-build-cache: true # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. # install-mode: "goinstall" 07070100000008000081B400000000000000000000000165DF4690000001AA000000000000000000000000000000000000002800000000uyuni-tools/.github/workflows/reuse.yml# SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. <https://fsfe.org> # # SPDX-License-Identifier: CC0-1.0 name: REUSE Compliance Check on: push: branches: - main pull_request: types: - opened - reopened - synchronize jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: REUSE Compliance Check uses: fsfe/reuse-action@v2 07070100000009000081B400000000000000000000000165DF46900000008D000000000000000000000000000000000000001700000000uyuni-tools/.gitignore# SPDX-FileCopyrightText: 2023 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 bin .vscode/ .idea/ vendor.tar.gz *.pyc __pycache__ **/tags 0707010000000A000081B400000000000000000000000165DF469000000514000000000000000000000000000000000000001A00000000uyuni-tools/.golangci.yml# SPDX-FileCopyrightText: 2024 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 run: tests: true skip-dirs: - vendor - examples linters-settings: dupl: enabled: true errcheck: enabled: true gofmt: enabled: true simplify: true goimports: enabled: true gocyclo: enabled: true min-complexity: 10 godot: enabled: true golint: enabled: true ineffassign: enabled: true maligned: enabled: true megacheck: enabled: true misspell: enabled: true revive: rules: - name: exported arguments: - disableStutteringCheck staticcheck: enabled: false stylecheck: enabled: true checks: ["ST1005", "ST1019"] structcheck: enabled: true typecheck: enabled: true unused: enabled: true varcheck: enabled: true whitespace: enabled: true linters: disable-all: true enable: - unused - dupl - errcheck - errname #- errorlint - godot - gofmt - goimports - gosimple #- gocyclo - revive - ineffassign - govet #- lll #- megacheck - misspell - revive #- staticcheck - stylecheck - typecheck #- unparam - unused - whitespace issues: include: - EXC0012 0707010000000B000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001300000000uyuni-tools/.reuse0707010000000C000081B400000000000000000000000165DF46900000030D000000000000000000000000000000000000001800000000uyuni-tools/.reuse/dep5Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: uyuni-tools Upstream-Contact: Uyuni Project <> Source: https://github.com/uyuni-project/uyuni-tools # Sample paragraph, commented out: # # Files: src/* # Copyright: $YEAR $NAME <$CONTACT> # License: ... Files: mgradm/shared/ssl/testdata/* Copyright: 2023 SUSE LLC License: Apache-2.0 Files: go.mod Copyright: 2023 SUSE LLC License: Apache-2.0 Files: go.sum Copyright: 2023 SUSE LLC License: Apache-2.0 Files: uyuni-tools.changes* Copyright: 2023 SUSE LLC License: Apache-2.0 Files: uyuni-tools.spec Copyright: 2023 SUSE LLC License: Apache-2.0 Files: .tito/tito.props Copyright: 2023 SUSE LLC License: Apache-2.0 Files: .tito/packages/* Copyright: 2023 SUSE LLC License: Apache-2.0 0707010000000D000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000001200000000uyuni-tools/.tito0707010000000E000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001900000000uyuni-tools/.tito/custom0707010000000F000081B400000000000000000000000165DF469000000726000000000000000000000000000000000000002300000000uyuni-tools/.tito/custom/custom.py# Copyright (c) 2018 SUSE Linux Products GmbH # SPDX-FileCopyrightText: 2023 SUSE LLC # # SPDX-License-Identifier: GPL-2.0-only """ Code for building packages in SUSE that need generated code not tracked in git. """ import os from tito.builder import Builder from tito.common import info_out, run_command, debug class SuseGitExtraGenerationBuilder(Builder): def _setup_sources(self): Builder._setup_sources(self) setup_execution_file_name = "setup.sh" setup_file_dir = os.path.join(self.git_root, self.relative_project_dir) setup_file_path = os.path.join(setup_file_dir, setup_execution_file_name) if os.path.exists(setup_file_path): info_out("Executing %s" % setup_file_path) output = run_command("[[ -x %s ]] && %s" % (setup_file_path, setup_file_path), True) filename = output.split('\n')[-1] if filename and os.path.exists(os.path.join(setup_file_dir, filename)): info_out("Copying %s to %s" % (os.path.join(setup_file_dir, filename), self.rpmbuild_sourcedir)) run_command("cp %s %s/" % (os.path.join(setup_file_dir, filename), self.rpmbuild_sourcedir), True) self.sources.append(os.path.join(self.rpmbuild_sourcedir, filename)) source_push = os.path.join(setup_file_dir, "push.sh") if os.path.exists(source_push): push_path = os.path.join(self.rpmbuild_sourcedir, "push.sh") run_command("cp %s %s/" % (source_push, self.rpmbuild_sourcedir), True) self.sources.append(push_path) run_command(f"sed '/^URL: .*$/aSource10000: push.sh' -i {self.spec_file}") cleanup = f"\nsed '/^Source10000: push.sh/d' -i $SRPM_PKG_DIR/{self.spec_file_name}" with open(push_path, "a") as fd: fd.write(cleanup) 07070100000010000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001B00000000uyuni-tools/.tito/packages07070100000011000081B400000000000000000000000165DF46900000000B000000000000000000000000000000000000002700000000uyuni-tools/.tito/packages/uyuni-tools0.1.4-1 ./ 07070100000012000081B400000000000000000000000165DF4690000000CB000000000000000000000000000000000000001D00000000uyuni-tools/.tito/tito.props[buildconfig] builder = custom.SuseGitExtraGenerationBuilder tagger = tito.tagger.SUSETagger changelog_with_email = 0 changelog_do_not_remove_cherrypick = 0 no_default_changelog = 1 lib_dir=.tito/custom 07070100000013000081B400000000000000000000000165DF469000000189000000000000000000000000000000000000001300000000uyuni-tools/.vimrc" SPDX-FileCopyrightText: 2023 SUSE LLC " " SPDX-License-Identifier: Apache-2.0 " Local vim configuration loaded by https://github.com/LucHermitte/local_vimrc " For local_vimrc to use this file, ensure .vimrc is in the g:local_vimrc " list. You can set it like the following in the vim or neovim config: " " let g:local_vimrc = ['.vimrc'] " Set make command set makeprg=go\ build\ ./... 07070100000014000081B400000000000000000000000165DF469000002C5D000000000000000000000000000000000000001400000000uyuni-tools/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. 07070100000015000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001500000000uyuni-tools/LICENSES07070100000016000081B400000000000000000000000165DF469000002828000000000000000000000000000000000000002400000000uyuni-tools/LICENSES/Apache-2.0.txtApache 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. 07070100000017000081B400000000000000000000000165DF469000001B88000000000000000000000000000000000000002100000000uyuni-tools/LICENSES/CC0-1.0.txtCreative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 07070100000018000081B400000000000000000000000165DF4690000043B9000000000000000000000000000000000000002600000000uyuni-tools/LICENSES/GPL-2.0-only.txtGNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice 07070100000019000081B400000000000000000000000165DF469000000C58000000000000000000000000000000000000001600000000uyuni-tools/README.md<!-- SPDX-FileCopyrightText: 2023 SUSE LLC SPDX-License-Identifier: Apache-2.0 --> [![REUSE status](https://api.reuse.software/badge/git.fsfe.org/reuse/api)](https://api.reuse.software/info/git.fsfe.org/reuse/api) # Tools to help using Uyuni as containers **These tools are work in progress** * `mgradm` used to help user administer Uyuni servers on K8s and Podman * `mgrctl` used to help user managing Uyuni servers mainly through its API # Deployment rolling release **NOTE:** This is rolling releases, meaning it can be broken at any time. Do not use it in production (yet!) ## For Podman deployment Requirement: - openSUSE Leap Micro 15.5 - Podman installed *Note that other distros with a recent Podman installed could work, but for now the tool is not packaged for them in OBS. So you would need to build it locally.* Add uyuni-tool repository: ``` zypper ar https://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable:/ContainerUtils/openSUSE_Leap_Micro_5.5/ uyuni-container-utils ``` Install `mgradm` package: `transactional-update pkg install mgradm` Run `mgradm` command to install Uyuni server on Podman: ``` mgradm install podman ``` If you build `uyuni-tools` on your machine, add the `--image registry.opensuse.org/systemsmanagement/uyuni/stable/containers/uyuni/server` option to the install command. This is not needed when using the package from OBS as it defaulting with this image at build time. **NOTE**: rolling image url is: registry.opensuse.org/systemsmanagement/uyuni/master/containers/uyuni/server Other sub-commands are also available. Explore the tool with the help command. A tool named `mgrctl` is also available with useful commands. ## K3s deployment For Look at a more details documentation at: https://github.com/uyuni-project/uyuni/tree/master/containers/doc/server-kubernetes # Development documentation ## Building `go build -o ./bin ./...` will produce the binaries in the root folder with `0.0.0` as version. To produce shell completion scripts for a given shell you can run: - `./bin/mgradm completion <shell> > $COMPLETION_FILE` for mgradm - `./bin/mgrctl error completion <shell> > $COMPLETION_FILE` for mgrctl You'll then need to source the resulting script(s). As an example, to enable bash completion for mgradm: `./bin/mgradm completion bash > ./bin/completion` `. ./bin/completion` The supported shells are: bash, zsh and fish. Alternatively, if you have `podman` installed you can run the `build.sh` script to build binaries compatible with any x86_64 linux. The version will be computed from the last git tag and offset from it. ### Building in Open Build Service In order to adjust the image, tag and chart to the project the package is built in, add the following at the end of the project configuration: ``` Macros: %_default_tag yourtag %_default_image theregistry.org/path/to/the/server %_default_chart oci://theregistry.org/path/to/the/chart :Macros ``` ### Disabling features at build time To disable features at build time pass the `-tags` parameter with the following values in a comma-separated list: * `nok8s`: will disable Kubernetes support 0707010000001A000081FD00000000000000000000000165DF46900000031A000000000000000000000000000000000000001500000000uyuni-tools/build.sh#!/usr/bin/bash # SPDX-FileCopyrightText: 2024 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 set -e mkdir -p ./bin tag=$(git describe --tags --abbrev=0) version=$(git describe --tags --abbrev=0 | cut -f 3 -d '-') offset=$(git rev-list --count ${tag}..) VERSION_NAME=github.com/uyuni-project/uyuni-tools/shared/utils.Version CGO_ENABLED=0 go build -ldflags "-X ${VERSION_NAME}=${tag}-${offset}" -o ./bin ./... for shell in "bash" "zsh" "fish"; do COMPLETION_FILE="./bin/completion.${shell}" # generate and source shell completion scripts for mgradm and mgrctl ./bin/mgradm completion ${shell} > "${COMPLETION_FILE}" ./bin/mgrctl completion ${shell} >> "${COMPLETION_FILE}" ./bin/mgrpxy completion ${shell} >> "${COMPLETION_FILE}" done golangci-lint run echo "DONE" 0707010000001B000081B400000000000000000000000165DF469000000425000000000000000000000000000000000000001300000000uyuni-tools/go.modmodule github.com/uyuni-project/uyuni-tools go 1.20 require github.com/spf13/cobra v1.1.3 require ( github.com/fsnotify/fsnotify v1.4.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/magiconair/properties v1.8.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/rs/zerolog v1.30.0 github.com/spf13/afero v1.1.2 // indirect github.com/spf13/cast v1.3.0 // indirect github.com/spf13/jwalterweatherman v1.0.0 // indirect github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.0 github.com/subosito/gotenv v1.2.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.10.0 golang.org/x/text v0.3.2 // indirect gopkg.in/ini.v1 v1.51.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 // indirect ) 0707010000001C000081B400000000000000000000000165DF469000007CCB000000000000000000000000000000000000001300000000uyuni-tools/go.sumcloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 0707010000001D000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000001300000000uyuni-tools/mgradm0707010000001E000041FD00000000000000000000000C65DF469000000000000000000000000000000000000000000000001700000000uyuni-tools/mgradm/cmd0707010000001F000081B400000000000000000000000165DF469000000B83000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/cmd/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package cmd import ( "os" "path" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/completion" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/distro" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/restart" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/start" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/stop" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/support" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/uninstall" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade" ) // NewCommand returns a new cobra.Command implementing the root command for kinder. func NewUyuniadmCommand() (*cobra.Command, error) { globalFlags := &types.GlobalFlags{} name := path.Base(os.Args[0]) rootCmd := &cobra.Command{ Use: name, Short: "Uyuni administration tool", Long: "Uyuni administration tool used to help user administer uyuni servers on kubernetes and podman", Version: utils.Version, SilenceUsage: true, // Don't show usage help on errors } usage, err := utils.GetUsageWithConfigHelpTemplate(rootCmd.UsageTemplate()) if err != nil { return rootCmd, err } rootCmd.SetUsageTemplate(usage) rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { utils.LogInit(true) utils.SetLogLevel(globalFlags.LogLevel) // do not log if running the completion cmd as the output is redirected to create a file to source if cmd.Name() != "completion" { log.Info().Msgf("Welcome to %s", name) log.Info().Msgf("Executing command: %s", cmd.Name()) } } rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", "configuration file path") rootCmd.PersistentFlags().StringVar(&globalFlags.LogLevel, "logLevel", "", "application log level (trace|debug|info|warn|error|fatal|panic)") migrateCmd := migrate.NewCommand(globalFlags) rootCmd.AddCommand(migrateCmd) installCmd := install.NewCommand(globalFlags) rootCmd.AddCommand(installCmd) rootCmd.AddCommand(uninstall.NewCommand(globalFlags)) distroCmd, err := distro.NewCommand(globalFlags) if err != nil { return rootCmd, err } rootCmd.AddCommand(distroCmd) rootCmd.AddCommand(completion.NewCommand(globalFlags)) rootCmd.AddCommand(support.NewCommand(globalFlags)) rootCmd.AddCommand(start.NewCommand(globalFlags)) rootCmd.AddCommand(restart.NewCommand(globalFlags)) rootCmd.AddCommand(stop.NewCommand(globalFlags)) rootCmd.AddCommand(inspect.NewCommand(globalFlags)) rootCmd.AddCommand(upgrade.NewCommand(globalFlags)) return rootCmd, err } 07070100000020000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/cmd/distro07070100000021000081B400000000000000000000000165DF469000000CDD000000000000000000000000000000000000002400000000uyuni-tools/mgradm/cmd/distro/cp.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package distro import ( "errors" "fmt" "os" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func umountAndRemove(mountpoint string) { umountCmd := []string{ "/usr/bin/umount", mountpoint, } if err := utils.RunCmd("/usr/bin/sudo", umountCmd...); err != nil { log.Fatal().Err(err).Msgf("Unable to unmount iso file, leaving %s intact", mountpoint) } os.Remove(mountpoint) } func registerDistro(connection *api.ConnectionDetails, distro *types.Distribution) error { client, err := api.Init(connection) if err != nil { log.Error().Msg("Unable to login and register the distribution. Manual distro registration is required.") return err } data := map[string]interface{}{ "treeLabel": distro.TreeLabel, "basePath": distro.BasePath, "channelLabel": distro.ChannelLabel, "installType": distro.InstallType, } _, err = client.Post("kickstart/tree/create", data) if err != nil { return fmt.Errorf("unable to register the distribution. Manual distro registration is required: %s", err) } log.Info().Msgf("Distribution %s successfully registered", distro.TreeLabel) return nil } func distroCp( globalFlags *types.GlobalFlags, flags *flagpole, cmd *cobra.Command, args []string, ) error { distroName := args[1] source := args[0] var channelLabel string if len(args) == 3 { channelLabel = args[2] } else { channelLabel = "" } cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter) log.Info().Msgf("Copying distribution %s\n", distroName) if !utils.FileExists(source) { return fmt.Errorf("source %s does not exists", source) } dstpath := "/srv/www/distributions/" + distroName if cnx.TestExistenceInPod(dstpath) { return fmt.Errorf("distribution already exists: %s", dstpath) } srcdir := source if strings.HasSuffix(source, ".iso") { log.Debug().Msg("Source is an iso file") tmpdir, err := os.MkdirTemp("", "mgrctl") if err != nil { return err } srcdir = tmpdir defer umountAndRemove(srcdir) mountCmd := []string{ "/usr/bin/mount", "-o", "ro,loop", source, srcdir, } if out, err := utils.RunCmdOutput(zerolog.DebugLevel, "/usr/bin/sudo", mountCmd...); err != nil { log.Debug().Msgf("Error mounting iso: '%s'", out) return errors.New("unable to mount iso file. Mount manually and try again") } } if err := cnx.Copy(srcdir, "server:"+dstpath, "tomcat", "susemanager"); err != nil { return fmt.Errorf("cannot copy %s: %s", dstpath, err) } log.Info().Msg("Distribution has been copied") if flags.ConnectionDetails.User != "" { distro := types.Distribution{ BasePath: dstpath, } if err := detectDistro(srcdir, channelLabel, flags, &distro); err != nil { return err } if err := registerDistro(&flags.ConnectionDetails, &distro); err != nil { return err } } return nil } 07070100000022000081B400000000000000000000000165DF469000000BB9000000000000000000000000000000000000002800000000uyuni-tools/mgradm/cmd/distro/detect.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package distro import ( "fmt" "path/filepath" "github.com/rs/zerolog/log" "github.com/spf13/viper" "github.com/uyuni-project/uyuni-tools/shared/types" ) var productMap = map[string]map[string]types.Distribution{ "AlmaLinux": { "9": { InstallType: "rhel_9", ChannelLabel: "almalinux9", }, "8": { InstallType: "rhel_8", ChannelLabel: "almalinux8", }, }, "SUSE Linux Enterprise": { "15 SP1": { InstallType: "sles15generic", ChannelLabel: "sle-product-sles15-sp1-pool", }, "15 SP2": { InstallType: "sles15generic", ChannelLabel: "sle-product-sles15-sp2-pool", }, "15 SP3": { InstallType: "sles15generic", ChannelLabel: "sle-product-sles15-sp3-pool", }, "15 SP4": { InstallType: "sles15generic", ChannelLabel: "sle-product-sles15-sp4-pool", }, "15 SP5": { InstallType: "sles15generic", ChannelLabel: "sle-product-sles15-sp5-pool", }, "12 SP5": { InstallType: "sles12generic", ChannelLabel: "sles12-sp5-pool", }, }, "Red Hat Enterprise Linux": { "7": { InstallType: "rhel_7", ChannelLabel: "rhel7-pool", }, "8": { InstallType: "rhel_8", ChannelLabel: "rhel8-pool", }, "9": { InstallType: "rhel_9", ChannelLabel: "rhel9-pool", }, }, } func getDistroFromDetails(distro string, version string, arch string, channeLabel string, flags *flagpole) (types.Distribution, error) { productFromConfig := flags.ProductMap var distribution types.Distribution var ok bool if productFromConfig[distro] != nil { distribution, ok = productFromConfig[distro][version] } else if productMap[distro] != nil { distribution, ok = productMap[distro][version] } if !ok { return types.Distribution{}, fmt.Errorf("unknown distribution, auto-registration is not possible") } if channeLabel != "" { distribution.ChannelLabel = channeLabel } else { distribution.ChannelLabel = fmt.Sprintf("%s-%s", distribution.ChannelLabel, arch) } return distribution, nil } func detectDistro(path string, channelLabel string, flags *flagpole, distro *types.Distribution) error { treeinfopath := filepath.Join(path, ".treeinfo") log.Trace().Msgf("Reading .treeinfo %s", treeinfopath) treeInfoViper := viper.New() treeInfoViper.SetConfigType("ini") treeInfoViper.SetConfigName(".treeinfo") treeInfoViper.AddConfigPath(path) if err := treeInfoViper.ReadInConfig(); err != nil { return err } dname := treeInfoViper.GetString("release.name") dversion := treeInfoViper.GetString("release.version") darch := treeInfoViper.GetString("general.arch") log.Debug().Msgf("Detected distro %s, version %s. arch %s", dname, dversion, darch) details, err := getDistroFromDetails(dname, dversion, darch, channelLabel, flags) if err != nil { return err } *distro = types.Distribution{ InstallType: details.InstallType, TreeLabel: dname, ChannelLabel: details.ChannelLabel, } return nil } 07070100000023000081B400000000000000000000000165DF46900000067D000000000000000000000000000000000000002800000000uyuni-tools/mgradm/cmd/distro/distro.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package distro import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type flagpole struct { Backend string ChannelLabel string ProductMap map[string]map[string]types.Distribution ConnectionDetails api.ConnectionDetails `mapstructure:"api"` } // NewCommand command for distribution management. func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) { var flags flagpole distroCmd := &cobra.Command{ Use: "distribution", Short: "Distribution management", Long: "Tools and utilities for distribution management", Aliases: []string{"distro"}, } cpCmd := &cobra.Command{ Use: "copy path-to-source distribution-name [channel-label]", Short: "Copy distribution files from iso to the container", Long: `Takes a path to iso file or directory with mounted iso and copies it into the container. Distribution name specifies the destination directory under /srv/www/distributions. Optional channel label specify which parent channel to associate with the distribution. Only when API details are provided and auto registration is done.`, Args: cobra.ExactArgs(2), Aliases: []string{"cp"}, RunE: func(cmd *cobra.Command, args []string) error { return utils.CommandHelper(globalFlags, cmd, args, &flags, distroCp) }, } if err := api.AddAPIFlags(distroCmd, true); err != nil { return distroCmd, err } distroCmd.AddCommand(cpCmd) return distroCmd, nil } 07070100000024000041FD00000000000000000000000365DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/inspect07070100000025000081B400000000000000000000000165DF4690000005D9000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/inspect/inspect.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package inspect import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type inspectFlags struct { Image string Tag string PullPolicy string } // NewCommand for extracting information from image and deployment. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { inspectCmd := &cobra.Command{ Use: "inspect", Short: "inspect", Long: "Extract information from image and deployment", Args: cobra.MaximumNArgs(0), RunE: func(cmd *cobra.Command, args []string) error { var flags inspectFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, inspect) }, } inspectCmd.SetUsageTemplate(inspectCmd.UsageTemplate()) inspectCmd.Flags().String("image", "", "Image. Leave it empty to analyze the current deployment") inspectCmd.Flags().String("tag", "", "Tag Image. Leave it empty to analyze the current deployment") utils.AddPullPolicyFlag(inspectCmd) if utils.KubernetesBuilt { utils.AddBackendFlag(inspectCmd) } return inspectCmd } func inspect(globalFlags *types.GlobalFlags, flags *inspectFlags, cmd *cobra.Command, args []string) error { fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanInspect, kuberneteInspect) if err != nil { return err } return fn(globalFlags, flags, cmd, args) } 07070100000026000081B400000000000000000000000165DF469000000FB6000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/cmd/inspect/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package inspect import ( "encoding/json" "fmt" "os" "os/exec" "path" "github.com/rs/zerolog/log" "github.com/spf13/cobra" inspect_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect/shared" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func kuberneteInspect( globalFlags *types.GlobalFlags, flags *inspectFlags, cmd *cobra.Command, args []string, ) error { serverImage, err := utils.ComputeImage(flags.Image, flags.Tag) if err != nil && len(serverImage) > 0 { return fmt.Errorf("failed to determine image. %s", err) } if len(serverImage) <= 0 { log.Debug().Msg("Use deployed image") cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter) serverImage, err = adm_utils.RunningImage(cnx, "uyuni") if err != nil { return fmt.Errorf("failed to find current running image: %s", err) } } inspectResult, err := InspectKubernetes(serverImage, flags.PullPolicy) if err != nil { return fmt.Errorf("inspect command failed %s", err) } prettyInspectOutput, err := json.MarshalIndent(inspectResult, "", " ") if err != nil { return fmt.Errorf("cannot print inspect result %s", err) } log.Info().Msgf("\n%s", string(prettyInspectOutput)) return nil } // InspectKubernetes check values on a given image and deploy. func InspectKubernetes(serverImage string, pullPolicy string) (map[string]string, error) { for _, binary := range []string{"kubectl", "helm"} { if _, err := exec.LookPath(binary); err != nil { return map[string]string{}, fmt.Errorf("install %s before running this command. %s", binary, err) } } scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return map[string]string{}, fmt.Errorf("failed to create temporary directory. %s", err) } if err := inspect_shared.GenerateInspectScript(scriptDir); err != nil { return map[string]string{}, err } command := path.Join(inspect_shared.InspectOutputFile.Directory, inspect_shared.InspectScriptFilename) const podName = "inspector" //delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong if err := shared_kubernetes.DeletePod(podName, shared_kubernetes.ServerFilter); err != nil { return map[string]string{}, fmt.Errorf("cannot delete %s: %s", podName, err) } //this is needed because folder with script needs to be mounted nodeName, err := shared_kubernetes.GetNode("uyuni") if err != nil { return map[string]string{}, fmt.Errorf("cannot find node for app uyuni %s", err) } //generate deploy data deployData := types.Deployment{ APIVersion: "v1", Spec: &types.Spec{ RestartPolicy: "Never", NodeName: nodeName, Containers: []types.Container{ { Name: podName, VolumeMounts: append(utils.PgsqlRequiredVolumeMounts, types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}), Image: serverImage, }, }, Volumes: append(utils.PgsqlRequiredVolumes, types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}), }, } //transform deploy data in JSON override, err := shared_kubernetes.GenerateOverrideDeployment(deployData) if err != nil { return map[string]string{}, err } err = shared_kubernetes.RunPod(podName, shared_kubernetes.ServerFilter, serverImage, pullPolicy, command, override) if err != nil { return map[string]string{}, fmt.Errorf("cannot run inspect pod %s", err) } inspectResult, err := inspect_shared.ReadInspectData(scriptDir) if err != nil { return map[string]string{}, fmt.Errorf("cannot inspect data. %s", err) } return inspectResult, err } 07070100000027000081B400000000000000000000000165DF46900000015D000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/inspect/nobuild.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package inspect import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kuberneteInspect( globalFlags *types.GlobalFlags, flags *inspectFlags, cmd *cobra.Command, args []string, ) error { return nil } 07070100000028000081B400000000000000000000000165DF469000000A97000000000000000000000000000000000000002900000000uyuni-tools/mgradm/cmd/inspect/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package inspect import ( "encoding/json" "fmt" "os" "github.com/rs/zerolog/log" "github.com/spf13/cobra" inspect_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/podman" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func podmanInspect( globalFlags *types.GlobalFlags, flags *inspectFlags, cmd *cobra.Command, args []string, ) error { serverImage, err := utils.ComputeImage(flags.Image, flags.Tag) if err != nil && len(serverImage) > 0 { return fmt.Errorf("failed to determine image. %s", err) } if len(serverImage) <= 0 { log.Debug().Msg("Use deployed image") cnx := shared.NewConnection("podman", shared_podman.ServerContainerName, "") serverImage, err = adm_utils.RunningImage(cnx, shared_podman.ServerContainerName) if err != nil { return fmt.Errorf("failed to find current running image") } } inspectResult, err := InspectPodman(serverImage, flags.PullPolicy) if err != nil { return fmt.Errorf("inspect command failed %s", err) } prettyInspectOutput, err := json.MarshalIndent(inspectResult, "", " ") if err != nil { return fmt.Errorf("cannot print inspect result %s", err) } log.Info().Msgf("\n%s", string(prettyInspectOutput)) return nil } // InspectPodman check values on a given image and deploy. func InspectPodman(serverImage string, pullPolicy string) (map[string]string, error) { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return map[string]string{}, fmt.Errorf("failed to create temporary directory %s", err) } extraArgs := []string{ "-v", scriptDir + ":" + inspect_shared.InspectOutputFile.Directory, "--security-opt", "label:disable", } err = shared_podman.PrepareImage(serverImage, pullPolicy) if err != nil { return map[string]string{}, err } if err := inspect_shared.GenerateInspectScript(scriptDir); err != nil { return map[string]string{}, err } err = podman.RunContainer("uyuni-inspect", serverImage, extraArgs, []string{inspect_shared.InspectOutputFile.Directory + "/" + inspect_shared.InspectScriptFilename}) if err != nil { return map[string]string{}, err } inspectResult, err := inspect_shared.ReadInspectData(scriptDir) if err != nil { return map[string]string{}, fmt.Errorf("cannot inspect data. %s", err) } return inspectResult, err } 07070100000029000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/inspect/shared0707010000002A000081B400000000000000000000000165DF4690000009FB000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/inspect/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "bytes" "fmt" "os" "path/filepath" "github.com/rs/zerolog/log" "github.com/spf13/viper" "github.com/uyuni-project/uyuni-tools/mgradm/shared/templates" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) var inspectValues = []types.InspectData{ types.NewInspectData("uyuni_release", "cat /etc/*release | grep 'Uyuni release' | cut -d ' ' -f3"), types.NewInspectData("suse_manager_release", "cat /etc/*release | grep 'SUSE Manager release' | cut -d ' ' -f4"), types.NewInspectData("fqdn", "cat /etc/rhn/rhn.conf | grep 'java.hostname' | cut -d' ' -f3"), types.NewInspectData("image_pg_version", "rpm -qa --qf '%{VERSION}\\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1"), types.NewInspectData("current_pg_version", "(test -e /var/lib/pgsql/data/PG_VERSION && cat /var/lib/pgsql/data/PG_VERSION) || true"), } // InspectOutputFile represents the directory and the basename where the inspect values are stored. var InspectOutputFile = types.InspectFile{ Directory: "/var/lib/uyuni-tools", Basename: "data", } // InspectScriptFilename is the inspect script basename. var InspectScriptFilename = "inspect.sh" // GenerateInspectScript create the inspect script. func GenerateInspectScript(scriptDir string) error { data := templates.InspectTemplateData{ Param: inspectValues, OutputFile: InspectOutputFile.Directory + "/" + InspectOutputFile.Basename, } scriptPath := filepath.Join(scriptDir, InspectScriptFilename) if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { return fmt.Errorf("failed to generate inspect script: %s", err) } return nil } // ReadInspectData returns a map with the values inspected by an image and deploy. func ReadInspectData(scriptDir string) (map[string]string, error) { path := filepath.Join(scriptDir, "data") log.Debug().Msgf("Trying to read %s", path) data, err := os.ReadFile(path) if err != nil { return map[string]string{}, fmt.Errorf("cannot parse file %s: %s", path, err) } inspectResult := make(map[string]string) viper.SetConfigType("env") if err := viper.ReadConfig(bytes.NewBuffer(data)); err != nil { return map[string]string{}, fmt.Errorf("cannot read config: %s", err) } for _, v := range inspectValues { if len(viper.GetString(v.Variable)) > 0 { inspectResult[v.Variable] = viper.GetString(v.Variable) } } return inspectResult, nil } 0707010000002B000041FD00000000000000000000000565DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/install0707010000002C000081B400000000000000000000000165DF469000000304000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/install/install.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package install import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/kubernetes" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand for installation. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { installCmd := &cobra.Command{ Use: "install", Short: "install a new server from scratch", Long: "Install a new server from scratch", } installCmd.AddCommand(podman.NewCommand(globalFlags)) if kubernetesCmd := kubernetes.NewCommand(globalFlags); kubernetesCmd != nil { installCmd.AddCommand(kubernetesCmd) } return installCmd } 0707010000002D000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/install/kubernetes0707010000002E000081B400000000000000000000000165DF4690000005CD000000000000000000000000000000000000003800000000uyuni-tools/mgradm/cmd/install/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared" cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type kubernetesInstallFlags struct { shared.InstallFlags `mapstructure:",squash"` Helm cmd_utils.HelmFlags } // NewCommand for kubernetes installation. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { kubernetesCmd := &cobra.Command{ Use: "kubernetes [fqdn]", Short: "install a new server on a kubernetes cluster from scratch", Long: `Install a new server on a kubernetes cluster from scratch The install command assumes the following: * kubectl is installed locally * a working kubeconfig should be set to connect to the cluster to deploy to The helm values file will be overridden with the values from the mgradm parameters or configuration. NOTE: for now installing on a remote cluster is not supported! `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var flags kubernetesInstallFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, installForKubernetes) }, } shared.AddInstallFlags(kubernetesCmd) cmd_utils.AddHelmInstallFlag(kubernetesCmd) return kubernetesCmd } 0707010000002F000081B400000000000000000000000165DF469000000124000000000000000000000000000000000000003500000000uyuni-tools/mgradm/cmd/install/kubernetes/nobuild.go// SPDX-FileCopyrightText: 2023 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { return nil } 07070100000030000081B400000000000000000000000165DF469000000A45000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/install/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package kubernetes import ( "fmt" "os/exec" "github.com/rs/zerolog" "github.com/spf13/cobra" install_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" ) func installForKubernetes(globalFlags *types.GlobalFlags, flags *kubernetesInstallFlags, cmd *cobra.Command, args []string, ) error { for _, binary := range []string{"kubectl", "helm"} { if _, err := exec.LookPath(binary); err != nil { return fmt.Errorf("install %s before running this command: %s", binary, err) } } flags.CheckParameters(cmd, "kubectl") cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter) fqdn := args[0] helmArgs := []string{"--set", "timezone=" + flags.TZ} if flags.MirrorPath != "" { // TODO Handle claims for multi-node clusters helmArgs = append(helmArgs, "--set", "mirror.hostPath="+flags.MirrorPath) } if flags.Debug.Java { helmArgs = append(helmArgs, "--set", "exposeJavaDebug=true") } // Check the kubernetes cluster setup clusterInfos := shared_kubernetes.CheckCluster() // Deploy the SSL CA or server certificate ca := ssl.SslPair{} sslArgs, err := kubernetes.DeployCertificate(&flags.Helm, &flags.Ssl, "", &ca, clusterInfos.GetKubeconfig(), fqdn, flags.Image.PullPolicy) if err != nil { return fmt.Errorf("cannot deploy certificate: %s", err) } helmArgs = append(helmArgs, sslArgs...) // Deploy Uyuni and wait for it to be up if err := kubernetes.Deploy(cnx, &flags.Image, &flags.Helm, &flags.Ssl, &clusterInfos, fqdn, flags.Debug.Java, helmArgs...); err != nil { return fmt.Errorf("cannot deploy uyuni: %s", err) } // Create setup script + env variables and copy it to the container envs := map[string]string{ "NO_SSL": "Y", } if err := install_shared.RunSetup(cnx, &flags.InstallFlags, args[0], envs); err != nil { return err } // The CA needs to be added to the database for Kickstart use. err = adm_utils.ExecCommand(zerolog.DebugLevel, cnx, "/usr/bin/rhn-ssl-dbstore", "--ca-cert=/etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT") if err != nil { return fmt.Errorf("error storing the SSL CA certificate in database: %s", err) } return nil } 07070100000031000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/install/podman07070100000032000081B400000000000000000000000165DF4690000004A5000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/install/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type podmanInstallFlags struct { shared.InstallFlags `mapstructure:",squash"` Podman podman.PodmanFlags } // NewCommand for podman installation. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { podmanCmd := &cobra.Command{ Use: "podman [fqdn]", Short: "install a new server on podman from scratch", Long: `Install a new server on podman from scratch The install podman command assumes podman is installed locally NOTE: for now installing on a remote podman is not supported! `, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var flags podmanInstallFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, installForPodman) }, } shared.AddInstallFlags(podmanCmd) podman.AddPodmanInstallFlag(podmanCmd) return podmanCmd } 07070100000033000081B400000000000000000000000165DF469000000DD1000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/install/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os/exec" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" install_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/install/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/podman" "github.com/uyuni-project/uyuni-tools/shared" shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func waitForSystemStart(cnx *shared.Connection, flags *podmanInstallFlags) error { // Setup the systemd service configuration options image := fmt.Sprintf("%s:%s", flags.Image.Name, flags.Image.Tag) podmanArgs := flags.Podman.Args if flags.MirrorPath != "" { podmanArgs = append(podmanArgs, "-v", flags.MirrorPath+":/mirror") } if err := podman.GenerateSystemdService(flags.TZ, image, flags.Debug.Java, podmanArgs); err != nil { return fmt.Errorf("cannot generate systemd service: %s", err) } log.Info().Msg("Waiting for the server to start...") if err := shared_podman.EnableService(shared_podman.ServerService); err != nil { return fmt.Errorf("cannot enable service: %s", err) } return cnx.WaitForServer() } func installForPodman( globalFlags *types.GlobalFlags, flags *podmanInstallFlags, cmd *cobra.Command, args []string, ) error { flags.CheckParameters(cmd, "podman") if _, err := exec.LookPath("podman"); err != nil { return fmt.Errorf("install podman before running this command: %s", err) } fqdn, err := getFqdn(args) if err != nil { return err } log.Info().Msgf("setting up server with the FQDN '%s'", fqdn) image, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag) if err != nil { return fmt.Errorf("failed to compute image URL, %s", err) } err = shared_podman.PrepareImage(image, flags.Image.PullPolicy) if err != nil { return err } cnx := shared.NewConnection("podman", shared_podman.ServerContainerName, "") if err := waitForSystemStart(cnx, flags); err != nil { return fmt.Errorf("cannot wait for system start: %s", err) } caPassword := flags.Ssl.Password if flags.Ssl.UseExisting() { // We need to have a password for the generated CA, even though it will be thrown away after install caPassword = "dummy" } env := map[string]string{ "CERT_O": flags.Ssl.Org, "CERT_OU": flags.Ssl.OU, "CERT_CITY": flags.Ssl.City, "CERT_STATE": flags.Ssl.State, "CERT_COUNTRY": flags.Ssl.Country, "CERT_EMAIL": flags.Ssl.Email, "CERT_CNAMES": strings.Join(append([]string{fqdn}, flags.Ssl.Cnames...), ","), "CERT_PASS": caPassword, } log.Info().Msg("run setup command in the container") if err := install_shared.RunSetup(cnx, &flags.InstallFlags, fqdn, env); err != nil { return err } if flags.Ssl.UseExisting() { if err := podman.UpdateSslCertificate(cnx, &flags.Ssl.Ca, &flags.Ssl.Server); err != nil { return fmt.Errorf("cannot update ssl certificate: %s", err) } } if err := shared_podman.EnablePodmanSocket(); err != nil { return fmt.Errorf("cannot enable podman socket: %s", err) } return nil } func getFqdn(args []string) (string, error) { if len(args) == 1 { return args[0], nil } else { fqdn_b, err := utils.RunCmdOutput(zerolog.DebugLevel, "hostname", "-f") if err != nil { return "", fmt.Errorf("failed to compute server FQDN: %s", err) } return strings.TrimSpace(string(fqdn_b)), nil } } 07070100000034000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/install/shared07070100000035000081B400000000000000000000000165DF46900000157D000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/install/shared/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "github.com/spf13/cobra" cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" apiTypes "github.com/uyuni-project/uyuni-tools/shared/api/types" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // DbFlags can store all values required to connect to a database. type DbFlags struct { Host string Name string Port int User string Password string Protocol string Provider string Admin struct { User string Password string } } // SccFlags can store SCC Credentials. type SccFlags struct { User string Password string } // DebugFlags contains information about enabled/disabled debug. type DebugFlags struct { Java bool } // InstallFlags stores all the flags used by install command. type InstallFlags struct { TZ string Email string EmailFrom string IssParent string MirrorPath string Tftp bool Db DbFlags ReportDb DbFlags Ssl cmd_utils.SslCertFlags Scc SccFlags Debug DebugFlags Image types.ImageFlags `mapstructure:",squash"` Admin apiTypes.User Organization string } // CheckParameters checks parameters for install command. func (flags *InstallFlags) CheckParameters(cmd *cobra.Command, command string) { utils.AskPasswordIfMissing(&flags.Db.Password, cmd.Flag("db-password").Usage) // Make sure we have all the required 3rd party flags or none flags.Ssl.CheckParameters() // Since we use cert-manager for self-signed certificates on kubernetes we don't need password for it if !flags.Ssl.UseExisting() && command == "podman" { utils.AskPasswordIfMissing(&flags.Ssl.Password, cmd.Flag("ssl-password").Usage) } // Use the host timezone if the user didn't define one if flags.TZ == "" { flags.TZ = utils.GetLocalTimezone() } utils.AskIfMissing(&flags.Email, cmd.Flag("email").Usage) utils.AskIfMissing(&flags.EmailFrom, cmd.Flag("emailfrom").Usage) } // AddInstallFlags add flags to installa command. func AddInstallFlags(cmd *cobra.Command) { cmd.Flags().String("tz", "", "Time zone to set on the server. Defaults to the host timezone") cmd.Flags().String("email", "admin@example.com", "Administrator e-mail") cmd.Flags().String("emailfrom", "admin@example.com", "E-Mail sending the notifications") cmd.Flags().String("mirrorPath", "", "Path to mirrored packages mounted on the host") cmd.Flags().String("issParent", "", "Inter Server Sync v1 parent fully qualified domain name") cmd.Flags().String("db-user", "spacewalk", "Database user") cmd.Flags().String("db-password", "", "Database password") cmd.Flags().String("db-name", "susemanager", "Database name") cmd.Flags().String("db-host", "localhost", "Database host") cmd.Flags().Int("db-port", 5432, "Database port") cmd.Flags().String("db-protocol", "tcp", "Database protocol") cmd.Flags().String("db-admin-user", "", "External database admin user name") cmd.Flags().String("db-admin-password", "", "External database admin password") cmd.Flags().String("db-provider", "", "External database provider. Possible values 'aws'") cmd.Flags().Bool("tftp", true, "Enable TFTP") cmd.Flags().String("reportdb-name", "reportdb", "Report database name") cmd.Flags().String("reportdb-host", "", "Report database host. Defaults to the selected FQDN") cmd.Flags().Int("reportdb-port", 5432, "Report database port") cmd.Flags().String("reportdb-user", "pythia_susemanager", "Report Database username") cmd.Flags().String("reportdb-password", "", "Report database password. Randomly generated by default") // For generated CA and certificate cmd.Flags().StringSlice("ssl-cname", []string{}, "SSL certificate cnames separated by commas") cmd.Flags().String("ssl-country", "DE", "SSL certificate country") cmd.Flags().String("ssl-state", "Bayern", "SSL certificate state") cmd.Flags().String("ssl-city", "Nuernberg", "SSL certificate city") cmd.Flags().String("ssl-org", "SUSE", "SSL certificate organization") cmd.Flags().String("ssl-ou", "SUSE", "SSL certificate organization unit") cmd.Flags().String("ssl-password", "", "Password for the CA certificate to generate") cmd.Flags().String("ssl-email", "ca-admin@example.com", "SSL certificate E-Mail") // For SSL 3rd party certificates cmd.Flags().StringSlice("ssl-ca-intermediate", []string{}, "Intermediate CA certificate path") cmd.Flags().String("ssl-ca-root", "", "Root CA certificate path") cmd.Flags().String("ssl-server-cert", "", "Server certificate path") cmd.Flags().String("ssl-server-key", "", "Server key path") cmd.Flags().String("scc-user", "", "SUSE Customer Center username") cmd.Flags().String("scc-password", "", "SUSE Customer Center password") cmd.Flags().Bool("debug-java", false, "Enable tomcat and taskomatic remote debugging") cmd_utils.AddImageFlag(cmd) cmd.Flags().String("admin-login", "admin", "Administrator user name") cmd.Flags().String("admin-password", "", "Administrator password. If empty, the first user will not be created") cmd.Flags().String("admin-firstName", "Administrator", "The first name of the administrator") cmd.Flags().String("admin-lastName", "McAdmin", "The last name of the administrator") cmd.Flags().String("admin-email", "root@localhost", "The administrator's email") cmd.Flags().String("organization", "Organiszation", "The first organization name") } 07070100000036000081B400000000000000000000000165DF469000000FA2000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/install/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "fmt" "os" "path/filepath" "strconv" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/mgradm/shared/templates" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/api/org" "github.com/uyuni-project/uyuni-tools/shared/utils" ) const setup_name = "setup.sh" // RunSetup execute the setup. func RunSetup(cnx *shared.Connection, flags *InstallFlags, fqdn string, env map[string]string) error { tmpFolder := generateSetupScript(flags, fqdn, env) defer os.RemoveAll(tmpFolder) if err := cnx.Copy(filepath.Join(tmpFolder, setup_name), "server:/tmp/setup.sh", "root", "root"); err != nil { return fmt.Errorf("cannot copy /tmp/setup.sh: %s", err) } err := adm_utils.ExecCommand(zerolog.InfoLevel, cnx, "/tmp/setup.sh") if err != nil { return fmt.Errorf("error running the setup script: %s", err) } // Call the org.createFirst api if flags are passed if flags.Admin.Password != "" { apiCnx := api.ConnectionDetails{ Server: fqdn, Insecure: true, // TODO Get the CA Cert and toggle this to false } _, err := org.CreateFirst(&apiCnx, flags.Organization, &flags.Admin) if err != nil { return err } } log.Info().Msg("Server set up") return nil } // generateSetupScript creates a temporary folder with the setup script to execute in the container. // The script exports all the needed environment variables and calls uyuni's mgr-setup. // Podman or kubernetes-specific variables can be passed using extraEnv parameter. func generateSetupScript(flags *InstallFlags, fqdn string, extraEnv map[string]string) string { localHostValues := []string{ "localhost", "127.0.0.1", "::1", fqdn, } localDb := utils.Contains(localHostValues, flags.Db.Host) dbHost := flags.Db.Host reportdbHost := flags.ReportDb.Host if localDb { dbHost = "localhost" if reportdbHost == "" { reportdbHost = "localhost" } } env := map[string]string{ "UYUNI_FQDN": fqdn, "MANAGER_USER": flags.Db.User, "MANAGER_PASS": flags.Db.Password, "MANAGER_ADMIN_EMAIL": flags.Email, "MANAGER_MAIL_FROM": flags.EmailFrom, "MANAGER_ENABLE_TFTP": boolToString(flags.Tftp), "LOCAL_DB": boolToString(localDb), "MANAGER_DB_NAME": flags.Db.Name, "MANAGER_DB_HOST": dbHost, "MANAGER_DB_PORT": strconv.Itoa(flags.Db.Port), "MANAGER_DB_PROTOCOL": flags.Db.Protocol, "REPORT_DB_NAME": flags.ReportDb.Name, "REPORT_DB_HOST": reportdbHost, "REPORT_DB_PORT": strconv.Itoa(flags.ReportDb.Port), "REPORT_DB_USER": flags.ReportDb.User, "REPORT_DB_PASS": flags.ReportDb.Password, "EXTERNALDB_ADMIN_USER": flags.Db.Admin.User, "EXTERNALDB_ADMIN_PASS": flags.Db.Admin.Password, "EXTERNALDB_PROVIDER": flags.Db.Provider, "ISS_PARENT": flags.IssParent, "ACTIVATE_SLP": "N", // Deprecated, will be removed soon "SCC_USER": flags.Scc.User, "SCC_PASS": flags.Scc.Password, } if flags.MirrorPath != "" { env["MIRROR_PATH"] = "/mirror" } // Add the extra environment variables for key, value := range extraEnv { env[key] = value } scriptDir, err := os.MkdirTemp("", "mgradm-*") if err != nil { log.Fatal().Err(err).Msg("Failed to create temporary directory") } dataTemplate := templates.MgrSetupScriptTemplateData{ Env: env, DebugJava: flags.Debug.Java, } scriptPath := filepath.Join(scriptDir, setup_name) if err = utils.WriteTemplateToFile(dataTemplate, scriptPath, 0555, true); err != nil { log.Fatal().Err(err).Msg("Failed to generate setup script") } return scriptDir } func boolToString(value bool) string { if value { return "Y" } return "N" } 07070100000037000041FD00000000000000000000000565DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/migrate07070100000038000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/migrate/kubernetes07070100000039000081B400000000000000000000000165DF4690000007F9000000000000000000000000000000000000003800000000uyuni-tools/mgradm/cmd/migrate/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared" cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type kubernetesMigrateFlags struct { shared.MigrateFlags `mapstructure:",squash"` Helm cmd_utils.HelmFlags Ssl cmd_utils.SslCertFlags } // NewCommand for kubernetes migration. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { migrateCmd := &cobra.Command{ Use: "kubernetes [source server FQDN]", Short: "migrate a remote server to containers running on a kubernetes cluster", Long: `Migrate a remote server to containers running on a kubernetes cluster This migration command assumes a few things: * the SSH configuration for the source server is complete, including user and all needed options to connect to the machine, * an SSH agent is started and the key to use to connect to the server is added to it, * kubectl is installed locally * A working kubeconfig should be set to connect to the cluster to deploy to When migrating a server with a automatically generate SSL Root CA certificate, the private key password will be required to convert it to RSA in order to be converted into a kubernetes secret. This is not needed if the source server does not have a generated SSL CA certificate. NOTE: for now installing on a remote cluster is not supported yet! `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var flags kubernetesMigrateFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, migrateToKubernetes) }, } shared.AddMigrateFlags(migrateCmd) cmd_utils.AddHelmInstallFlag(migrateCmd) migrateCmd.Flags().String("ssl-password", "", "SSL CA generated private key password") return migrateCmd } 0707010000003A000081B400000000000000000000000165DF469000000124000000000000000000000000000000000000003500000000uyuni-tools/mgradm/cmd/migrate/kubernetes/nobuild.go// SPDX-FileCopyrightText: 2023 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { return nil } 0707010000003B000081B400000000000000000000000165DF46900000183A000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/migrate/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package kubernetes import ( "encoding/base64" "fmt" "os" "os/exec" "path" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" migration_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func migrateToKubernetes( globalFlags *types.GlobalFlags, flags *kubernetesMigrateFlags, cmd *cobra.Command, args []string, ) error { for _, binary := range []string{"kubectl", "helm"} { if _, err := exec.LookPath(binary); err != nil { return fmt.Errorf("install %s before running this command: %s", binary, err) } } cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter) fqdn := args[0] serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag) if err != nil { return fmt.Errorf("failed to compute image URL") } // Find the SSH Socket and paths for the migration sshAuthSocket := migration_shared.GetSshAuthSocket() sshConfigPath, sshKnownhostsPath := migration_shared.GetSshPaths() // Prepare the migration script and folder scriptDir, err := adm_utils.GenerateMigrationScript(fqdn, true) if err != nil { return fmt.Errorf("failed to generater migration script: %s", err) } defer os.RemoveAll(scriptDir) // Install Uyuni with generated CA cert: an empty struct means no 3rd party cert var sslFlags adm_utils.SslCertFlags // We don't need the SSL certs at this point of the migration clusterInfos := shared_kubernetes.CheckCluster() kubeconfig := clusterInfos.GetKubeconfig() //TODO: check if we need to handle SELinux policies, as we do in podman if err := kubernetes.Deploy(cnx, &flags.Image, &flags.Helm, &sslFlags, &clusterInfos, fqdn, false, "--set", "migration.ssh.agentSocket="+sshAuthSocket, "--set", "migration.ssh.configPath="+sshConfigPath, "--set", "migration.ssh.knownHostsPath="+sshKnownhostsPath, "--set", "migration.dataPath="+scriptDir); err != nil { return fmt.Errorf("cannot run deploy: %s", err) } // Run the actual migration if err := adm_utils.RunMigration(cnx, scriptDir, "migrate.sh"); err != nil { return fmt.Errorf("cannot run migration: %s", err) } tz, oldPgVersion, newPgVersion, err := adm_utils.ReadContainerData(scriptDir) if err != nil { return fmt.Errorf("cannot read data from container: %s", err) } helmArgs := []string{ "--reset-values", "--set", "timezone=" + tz, } //this is needed because folder with script needs to be mounted //check the node before scaling down nodeName, err := shared_kubernetes.GetNode("uyuni") if err != nil { return fmt.Errorf("cannot find node for app uyuni %s", err) } if err := kubernetes.RunPostUpgradeScript(serverImage, flags.Image.PullPolicy, nodeName); err != nil { return fmt.Errorf("cannot run post upgrade script: %s", err) } if oldPgVersion != newPgVersion { log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", oldPgVersion, newPgVersion) if err := kubernetes.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, nodeName, oldPgVersion, newPgVersion); err != nil { return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err) } } schemaUpdateRequired := oldPgVersion != newPgVersion if err := kubernetes.RunPgsqlFinalizeScript(serverImage, flags.Image.PullPolicy, nodeName, schemaUpdateRequired); err != nil { return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err) } if err := kubernetes.RunPostUpgradeScript(serverImage, flags.Image.PullPolicy, nodeName); err != nil { return fmt.Errorf("cannot run post upgrade script: %s", err) } setupSslArray, err := setupSsl(&flags.Helm, kubeconfig, scriptDir, flags.Ssl.Password, flags.Image.PullPolicy) if err != nil { return fmt.Errorf("cannot setup SSL: %s", err) } helmArgs = append(helmArgs, setupSslArray...) // As we upgrade the helm instance without the migration parameters the SSL certificate will be used if err := kubernetes.UyuniUpgrade(serverImage, flags.Image.PullPolicy, &flags.Helm, kubeconfig, fqdn, clusterInfos.Ingress, helmArgs...); err != nil { return fmt.Errorf("cannot run uyuni upgrade: %s", err) } return nil } // updateIssuer replaces the temporary SSL certificate issuer with the source server CA. // Return additional helm args to use the SSL certificates. func setupSsl(helm *adm_utils.HelmFlags, kubeconfig string, scriptDir string, password string, pullPolicy string) ([]string, error) { caCert := path.Join(scriptDir, "RHN-ORG-TRUSTED-SSL-CERT") caKey := path.Join(scriptDir, "RHN-ORG-PRIVATE-SSL-KEY") if utils.FileExists(caCert) && utils.FileExists(caKey) { key := base64.StdEncoding.EncodeToString(ssl.GetRsaKey(caKey, password)) // Strip down the certificate text part out, err := utils.RunCmdOutput(zerolog.DebugLevel, "openssl", "x509", "-in", caCert) if err != nil { return []string{}, fmt.Errorf("failed to strip text part of CA certificate %s", err) } cert := base64.StdEncoding.EncodeToString(out) ca := ssl.SslPair{Cert: cert, Key: key} // An empty struct means no third party certificate sslFlags := adm_utils.SslCertFlags{} ret, err := kubernetes.DeployCertificate(helm, &sslFlags, cert, &ca, kubeconfig, "", pullPolicy) if err != nil { return []string{}, fmt.Errorf("cannot deploy certificate: %s", err) } return ret, nil } else { // Handle third party certificates and CA sslFlags := adm_utils.SslCertFlags{ Ca: ssl.CaChain{Root: caCert}, Server: ssl.SslPair{ Key: path.Join(scriptDir, "spacewalk.key"), Cert: path.Join(scriptDir, "spacewalk.crt"), }, } kubernetes.DeployExistingCertificate(helm, &sslFlags, kubeconfig) } return []string{}, nil } 0707010000003C000081B400000000000000000000000165DF46900000031E000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/migrate/migrate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package migrate import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/kubernetes" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand for migration. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { migrateCmd := &cobra.Command{ Use: "migrate [source server FQDN]", Short: "migrate a remote server to containers", Long: "Migrate a remote server to containers", } migrateCmd.AddCommand(podman.NewCommand(globalFlags)) if kubernetesCmd := kubernetes.NewCommand(globalFlags); kubernetesCmd != nil { migrateCmd.AddCommand(kubernetesCmd) } return migrateCmd } 0707010000003D000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/migrate/podman0707010000003E000081B400000000000000000000000165DF4690000005CD000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/migrate/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared" podman_utils "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type podmanMigrateFlags struct { shared.MigrateFlags `mapstructure:",squash"` Podman podman_utils.PodmanFlags } // NewCommand for podman migration. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { migrateCmd := &cobra.Command{ Use: "podman [source server FQDN]", Short: "migrate a remote server to containers running on podman", Long: `Migrate a remote server to containers running on podman This migration command assumes a few things: * the SSH configuration for the source server is complete, including user and all needed options to connect to the machine, * an SSH agent is started and the key to use to connect to the server is added to it, * podman is installed locally NOTE: for now installing on a remote podman is not supported yet! `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var flags podmanMigrateFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, migrateToPodman) }, } shared.AddMigrateFlags(migrateCmd) podman_utils.AddPodmanInstallFlag(migrateCmd) return migrateCmd } 0707010000003F000081B400000000000000000000000165DF46900000098A000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/migrate/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os/exec" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" migration_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/migrate/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/podman" podman_utils "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func migrateToPodman(globalFlags *types.GlobalFlags, flags *podmanMigrateFlags, cmd *cobra.Command, args []string) error { if _, err := exec.LookPath("podman"); err != nil { log.Fatal().Err(err).Msg("install podman before running this command") } sourceFqdn := args[0] serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag) if err != nil { return fmt.Errorf("cannot compute image: %s", err) } // Find the SSH Socket and paths for the migration sshAuthSocket := migration_shared.GetSshAuthSocket() sshConfigPath, sshKnownhostsPath := migration_shared.GetSshPaths() tz, oldPgVersion, newPgVersion, err := podman.RunMigration(serverImage, flags.Image.PullPolicy, sshAuthSocket, sshConfigPath, sshKnownhostsPath, sourceFqdn) if err != nil { return fmt.Errorf("cannot run migration script: %s", err) } if oldPgVersion != newPgVersion { if err := podman.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, oldPgVersion, newPgVersion); err != nil { return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err) } } schemaUpdateRequired := oldPgVersion != newPgVersion if err := podman.RunPgsqlFinalizeScript(serverImage, schemaUpdateRequired); err != nil { return fmt.Errorf("cannot run PostgreSQL finalize script: %s", err) } if err := podman.RunPostUpgradeScript(serverImage); err != nil { return fmt.Errorf("cannot run post upgrade script: %s", err) } if err := podman.GenerateSystemdService(tz, serverImage, false, viper.GetStringSlice("podman.arg")); err != nil { return fmt.Errorf("cannot generate systemd service file: %s", err) } // Start the service if err := podman_utils.EnableService(podman_utils.ServerService); err != nil { return err } log.Info().Msg("Server migrated") if err := podman_utils.EnablePodmanSocket(); err != nil { return fmt.Errorf("cannot run enable podman socket: %s", err) } return nil } 07070100000040000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/migrate/shared07070100000041000081B400000000000000000000000165DF46900000026B000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/migrate/shared/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/types" ) // MigrateFlags represents flag required by migration command. type MigrateFlags struct { Image types.ImageFlags `mapstructure:",squash"` MigrationImage types.ImageFlags `mapstructure:"migration"` } // AddMigrateFlags add migration flags to a command. func AddMigrateFlags(cmd *cobra.Command) { utils.AddImageFlag(cmd) utils.AddMigrationImageFlag(cmd) } 07070100000042000081B400000000000000000000000165DF469000000431000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/migrate/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "os" "path/filepath" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // GetSshAuthSocket returns the SSH_AUTH_SOCK environment variable value. func GetSshAuthSocket() string { path := os.Getenv("SSH_AUTH_SOCK") if len(path) == 0 { log.Fatal().Msg("SSH_AUTH_SOCK is not defined, start an ssh agent and try again") } return path } // GetSshPaths returns the user SSH config and known_hosts paths. func GetSshPaths() (string, string) { // Find ssh config to mount it in the container homedir, err := os.UserHomeDir() if err != nil { log.Fatal().Msg("Failed to find home directory to look for SSH config") } sshConfigPath := filepath.Join(homedir, ".ssh", "config") sshKnownhostsPath := filepath.Join(homedir, ".ssh", "known_hosts") if !utils.FileExists(sshConfigPath) { sshConfigPath = "" } if !utils.FileExists(sshKnownhostsPath) { sshKnownhostsPath = "" } return sshConfigPath, sshKnownhostsPath } 07070100000043000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/restart07070100000044000081B400000000000000000000000165DF4690000001C1000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/cmd/restart/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package restart import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kubernetesRestart( globalFlags *types.GlobalFlags, flags *restartFlags, cmd *cobra.Command, args []string, ) error { return kubernetes.Restart(kubernetes.ServerFilter) } 07070100000045000081B400000000000000000000000165DF469000000191000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/restart/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package restart import ( "fmt" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kubernetesRestart( globalFlags *types.GlobalFlags, flags *restartFlags, cmd *cobra.Command, args []string, ) error { return fmt.Errorf("built without kubernetes support") } 07070100000046000081B400000000000000000000000165DF4690000001A6000000000000000000000000000000000000002900000000uyuni-tools/mgradm/cmd/restart/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package restart import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) func podmanRestart( globalFlags *types.GlobalFlags, flags *restartFlags, cmd *cobra.Command, args []string, ) error { return podman.RestartService(podman.ServerService) } 07070100000047000081B400000000000000000000000165DF46900000048C000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/restart/restart.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package restart import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type restartFlags struct { Backend string } // NewCommand to restart server. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { restartCmd := &cobra.Command{ Use: "restart", Short: "restart the server", Long: "Restart the server", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { var flags restartFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, restart) }, } restartCmd.SetUsageTemplate(restartCmd.UsageTemplate()) if utils.KubernetesBuilt { utils.AddBackendFlag(restartCmd) } return restartCmd } func restart(globalFlags *types.GlobalFlags, flags *restartFlags, cmd *cobra.Command, args []string) error { fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanRestart, kubernetesRestart) if err != nil { return err } return fn(globalFlags, flags, cmd, args) } 07070100000048000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001D00000000uyuni-tools/mgradm/cmd/start07070100000049000081B400000000000000000000000165DF4690000001B9000000000000000000000000000000000000002B00000000uyuni-tools/mgradm/cmd/start/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package start import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kubernetesStart( globalFlags *types.GlobalFlags, flags *startFlags, cmd *cobra.Command, args []string, ) error { return kubernetes.Start(kubernetes.ServerFilter) } 0707010000004A000081B400000000000000000000000165DF46900000018B000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/cmd/start/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package start import ( "fmt" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kubernetesStart( globalFlags *types.GlobalFlags, flags *startFlags, cmd *cobra.Command, args []string, ) error { return fmt.Errorf("built without kubernetes support") } 0707010000004B000081B400000000000000000000000165DF46900000019E000000000000000000000000000000000000002700000000uyuni-tools/mgradm/cmd/start/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package start import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) func podmanStart( globalFlags *types.GlobalFlags, flags *startFlags, cmd *cobra.Command, args []string, ) error { return podman.StartService(podman.ServerService) } 0707010000004C000081B400000000000000000000000165DF46900000046C000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/start/start.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package start import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type startFlags struct { Backend string } // NewCommand starts the server. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { startCmd := &cobra.Command{ Use: "start", Short: "start the server", Long: "Start the server", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { var flags startFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, start) }, } startCmd.SetUsageTemplate(startCmd.UsageTemplate()) if utils.KubernetesBuilt { utils.AddBackendFlag(startCmd) } return startCmd } func start(globalFlags *types.GlobalFlags, flags *startFlags, cmd *cobra.Command, args []string) error { fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanStart, kubernetesStart) if err != nil { return err } return fn(globalFlags, flags, cmd, args) } 0707010000004D000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001C00000000uyuni-tools/mgradm/cmd/stop0707010000004E000081B400000000000000000000000165DF4690000001B5000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/stop/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package stop import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kubernetesStop( globalFlags *types.GlobalFlags, flags *stopFlags, cmd *cobra.Command, args []string, ) error { return kubernetes.Stop(kubernetes.ServerFilter) } 0707010000004F000081B400000000000000000000000165DF469000000188000000000000000000000000000000000000002C00000000uyuni-tools/mgradm/cmd/stop/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package stop import ( "fmt" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func kubernetesStop( globalFlags *types.GlobalFlags, flags *stopFlags, cmd *cobra.Command, args []string, ) error { return fmt.Errorf("built without kubernetes support") } 07070100000050000081B400000000000000000000000165DF46900000019A000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/stop/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package stop import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) func podmanStop( globalFlags *types.GlobalFlags, flags *stopFlags, cmd *cobra.Command, args []string, ) error { return podman.StopService(podman.ServerService) } 07070100000051000081B400000000000000000000000165DF46900000045A000000000000000000000000000000000000002400000000uyuni-tools/mgradm/cmd/stop/stop.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package stop import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type stopFlags struct { Backend string } // NewCommand to stop server. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { stopCmd := &cobra.Command{ Use: "stop", Short: "stop the server", Long: "Stop the server", Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { var flags stopFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, stop) }, } stopCmd.SetUsageTemplate(stopCmd.UsageTemplate()) if utils.KubernetesBuilt { utils.AddBackendFlag(stopCmd) } return stopCmd } func stop(globalFlags *types.GlobalFlags, flags *stopFlags, cmd *cobra.Command, args []string) error { fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), podmanStop, kubernetesStop) if err != nil { return err } return fn(globalFlags, flags, cmd, args) } 07070100000052000041FD00000000000000000000000365DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/support07070100000053000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/support/config07070100000054000081B400000000000000000000000165DF4690000003C4000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/support/config/config.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package config import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type configFlags struct { Output string Backend string } // NewCommand is the command for creates supportconfig. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { configCmd := &cobra.Command{ Use: "config", Short: "extract configuration and logs", Long: `Extract the host or cluster configuration and logs as well as those from the containers for support to help debugging.`, RunE: func(cmd *cobra.Command, args []string) error { var flags configFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, extract) }, } configCmd.Flags().StringP("output", "o", "supportconfig.tar.gz", "path where to extract the data") utils.AddBackendFlag(configCmd) return configCmd } 07070100000055000081B400000000000000000000000165DF469000000C62000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/support/config/extractor.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package config import ( "errors" "fmt" "os" "os/exec" "path" "regexp" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func extract(globalFlags *types.GlobalFlags, flags *configFlags, cmd *cobra.Command, args []string) error { cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter) // Copy the generated file locally tmpDir, err := os.MkdirTemp("", "mgradm-*") if err != nil { return fmt.Errorf("failed to create temporary directory: %s", err) } defer os.RemoveAll(tmpDir) var files []string extensions := []string{"", ".md5"} // Run supportconfig in the container if it's running log.Info().Msg("Running supportconfig in the container") out, err := cnx.Exec("supportconfig") if err != nil { return errors.New("failed to run supportconfig") } else { tarballPath := getSupportConfigPath(out) if tarballPath == "" { return fmt.Errorf("failed to find container supportconfig tarball from command output") } // TODO Get the error from copy for _, ext := range extensions { containerTarball := path.Join(tmpDir, "container-supportconfig.txz"+ext) if err := cnx.Copy("server:"+tarballPath+ext, containerTarball, "", ""); err != nil { return fmt.Errorf("cannot copy tarball: %s", err) } files = append(files, containerTarball) // Remove the generated file in the container if _, err := cnx.Exec("rm", tarballPath+ext); err != nil { return fmt.Errorf("failed to remove %s%s file in the container: %s", tarballPath, ext, err) } } } // Run supportconfig on the host if installed if _, err := exec.LookPath("supportconfig"); err == nil { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "supportconfig") if err != nil { return fmt.Errorf("failed to run supportconfig on the host: %s", err) } tarballPath := getSupportConfigPath(out) // Look for the generated supportconfig file if tarballPath != "" && utils.FileExists(tarballPath) { for _, ext := range extensions { files = append(files, tarballPath+ext) } } else { return errors.New("failed to find host supportconfig tarball from command output") } } else { log.Warn().Msg("supportconfig is not available on the host, skipping it") } // TODO Get cluster infos in case of kubernetes // Pack it all into a tarball log.Info().Msg("Preparing the tarball") tarball, err := utils.NewTarGz(flags.Output) if err != nil { return err } for _, file := range files { if err := tarball.AddFile(file, path.Base(file)); err != nil { return fmt.Errorf("failed to add %s to tarball: %s", path.Base(file), err) } } tarball.Close() return nil } func getSupportConfigPath(out []byte) string { re := regexp.MustCompile(`/var/log/scc_[^.]+\.txz`) return re.FindString(string(out)) } 07070100000056000081B400000000000000000000000165DF469000000245000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/support/support.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package support import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/support/config" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand to export supportconfig. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { supportCmd := &cobra.Command{ Use: "support", Short: "commands for support operations", Long: "Commands for support operations", } supportCmd.AddCommand(config.NewCommand(globalFlags)) return supportCmd } 07070100000057000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002100000000uyuni-tools/mgradm/cmd/uninstall07070100000058000081B400000000000000000000000165DF469000000A0B000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/uninstall/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package uninstall import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func uninstallForKubernetes( globalFlags *types.GlobalFlags, flags *uninstallFlags, cmd *cobra.Command, args []string, ) error { clusterInfos := kubernetes.CheckCluster() kubeconfig := clusterInfos.GetKubeconfig() // TODO Find all the PVs related to the server if we want to delete them // Uninstall uyuni namespace := kubernetes.HelmUninstall(kubeconfig, "uyuni", "", flags.DryRun) // Remove the remaining configmap and secrets if namespace != "" { _, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", "-n", namespace, "get", "secret", "uyuni-ca") caSecret := "uyuni-ca" if err != nil { caSecret = "" } if flags.DryRun { log.Info().Msgf("Would run kubectl delete -n %s configmap uyuni-ca", namespace) log.Info().Msgf("Would run kubectl delete -n %s secret uyuni-cert %s", namespace, caSecret) } else { log.Info().Msgf("Running kubectl delete -n %s configmap uyuni-ca", namespace) if err := utils.RunCmd("kubectl", "delete", "-n", namespace, "configmap", "uyuni-ca"); err != nil { log.Info().Err(err).Msgf("Failed deleting config map") } log.Info().Msgf("Running kubectl delete -n %s secret uyuni-cert %s", namespace, caSecret) args := []string{"delete", "-n", namespace, "secret", "uyuni-cert"} if caSecret != "" { args = append(args, caSecret) } err := utils.RunCmd("kubectl", args...) if err != nil { log.Info().Err(err).Msgf("Failed deleting secret") } } } // TODO Remove the PVs or wait for their automatic removal if purge is requested // Also wait if the PVs are dynamic with Delete reclaim policy but the user didn't ask to purge them // Since some storage plugins don't handle Delete policy, we may need to check for error events to avoid infinite loop // Uninstall cert-manager if we installed it kubernetes.HelmUninstall(kubeconfig, "cert-manager", "-linstalledby=mgradm", flags.DryRun) // Remove the K3s Traefik config if clusterInfos.IsK3s() { kubernetes.UninstallK3sTraefikConfig(flags.DryRun) } // Remove the rke2 nginx config if clusterInfos.IsRke2() { kubernetes.UninstallRke2NginxConfig(flags.DryRun) } return nil } const kubernetesHelp = kubernetes.UninstallHelp 07070100000059000081B400000000000000000000000165DF469000000182000000000000000000000000000000000000003100000000uyuni-tools/mgradm/cmd/uninstall/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package uninstall import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func uninstallForKubernetes( globalFlags *types.GlobalFlags, flags *uninstallFlags, cmd *cobra.Command, args []string, ) error { return nil } const kubernetesHelp = "" 0707010000005A000081B400000000000000000000000165DF469000000461000000000000000000000000000000000000002B00000000uyuni-tools/mgradm/cmd/uninstall/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package uninstall import ( "fmt" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func uninstallForPodman( globalFlags *types.GlobalFlags, flags *uninstallFlags, cmd *cobra.Command, args []string, ) error { // Uninstall the service podman.UninstallService("uyuni-server", flags.DryRun) // Force stop the pod podman.DeleteContainer(podman.ServerContainerName, flags.DryRun) // Remove the volumes if flags.PurgeVolumes { volumes := []string{"cgroup"} for _, volume := range utils.ServerVolumeMounts { volumes = append(volumes, volume.Name) } for _, volume := range volumes { if err := podman.DeleteVolume(volume, flags.DryRun); err != nil { return fmt.Errorf("cannot delete volume %s: %s", volume, err) } } log.Info().Msg("All volumes removed") } podman.DeleteNetwork(flags.DryRun) return podman.ReloadDaemon(flags.DryRun) } 0707010000005B000081B400000000000000000000000165DF4690000005A6000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/cmd/uninstall/uninstall.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package uninstall import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type uninstallFlags struct { Backend string DryRun bool PurgeVolumes bool } // NewCommand uninstall a server and optionally the corresponding volumes. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { uninstallCmd := &cobra.Command{ Use: "uninstall", Short: "uninstall a server", Long: "Uninstall a server and optionally the corresponding volumes." + kubernetesHelp, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { var flags uninstallFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, uninstall) }, } uninstallCmd.Flags().BoolP("dryRun", "n", false, "Only show what would be done") uninstallCmd.Flags().Bool("purgeVolumes", false, "Also remove the volume") if utils.KubernetesBuilt { utils.AddBackendFlag(uninstallCmd) } return uninstallCmd } func uninstall( globalFlags *types.GlobalFlags, flags *uninstallFlags, cmd *cobra.Command, args []string, ) error { fn, err := shared.ChoosePodmanOrKubernetes(cmd.Flags(), uninstallForPodman, uninstallForKubernetes) if err != nil { return err } return fn(globalFlags, flags, cmd, args) } 0707010000005C000041FD00000000000000000000000565DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgradm/cmd/upgrade0707010000005D000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/upgrade/kubernetes0707010000005E000081B400000000000000000000000165DF46900000044F000000000000000000000000000000000000003800000000uyuni-tools/mgradm/cmd/upgrade/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared" cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type kubernetesUpgradeFlags struct { shared.UpgradeFlags `mapstructure:",squash"` Helm cmd_utils.HelmFlags } // NewCommand to upgrade a kubernetes server. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { upgradeCmd := &cobra.Command{ Use: "kubernetes", Short: "upgrade a local server on kubernetes", Long: `Upgrade a local server on kubernetes `, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { var flags kubernetesUpgradeFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, upgradeKubernetes) }, } shared.AddUpgradeFlags(upgradeCmd) cmd_utils.AddHelmInstallFlag(upgradeCmd) return upgradeCmd } 0707010000005F000081B400000000000000000000000165DF469000000124000000000000000000000000000000000000003500000000uyuni-tools/mgradm/cmd/upgrade/kubernetes/nobuild.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { return nil } 07070100000060000081B400000000000000000000000165DF469000000E90000000000000000000000000000000000000003300000000uyuni-tools/mgradm/cmd/upgrade/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package kubernetes import ( "fmt" "os" "os/exec" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect" upgrade_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared" shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func upgradeKubernetes( globalFlags *types.GlobalFlags, flags *kubernetesUpgradeFlags, cmd *cobra.Command, args []string, ) error { for _, binary := range []string{"kubectl", "helm"} { if _, err := exec.LookPath(binary); err != nil { return fmt.Errorf("install %s before running this command", binary) } } cnx := shared.NewConnection("kubectl", "", shared_kubernetes.ServerFilter) serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag) if err != nil { return fmt.Errorf("failed to compute image URL") } inspectedValues, err := inspect.InspectKubernetes(serverImage, flags.Image.PullPolicy) if err != nil { return err } err = upgrade_shared.SanityCheck(cnx, inspectedValues, serverImage) if err != nil { return err } fqdn, exist := inspectedValues["fqdn"] if !exist { return fmt.Errorf("inspect function did non return fqdn value") } clusterInfos := shared_kubernetes.CheckCluster() kubeconfig := clusterInfos.GetKubeconfig() scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } //this is needed because folder with script needs to be mounted //check the node before scaling down nodeName, err := shared_kubernetes.GetNode("uyuni") if err != nil { return fmt.Errorf("cannot find node for app uyuni %s", err) } err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 0) if err != nil { return fmt.Errorf("cannot set replica to 0: %s", err) } defer func() { // if something is running, we don't need to set replicas to 1 if _, err = shared_kubernetes.GetNode("uyuni"); err != nil { err = shared_kubernetes.ReplicasTo(shared_kubernetes.ServerFilter, 1) } }() if inspectedValues["image_pg_version"] > inspectedValues["current_pg_version"] { log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]) if err := kubernetes.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, nodeName, inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]); err != nil { return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err) } } schemaUpdateRequired := inspectedValues["current_pg_version"] != inspectedValues["image_pg_version"] if err := kubernetes.RunPgsqlFinalizeScript(serverImage, flags.Image.PullPolicy, nodeName, schemaUpdateRequired); err != nil { return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err) } if err := kubernetes.RunPostUpgradeScript(serverImage, flags.Image.PullPolicy, nodeName); err != nil { return fmt.Errorf("cannot run post upgrade script: %s", err) } err = kubernetes.UyuniUpgrade(serverImage, flags.Image.PullPolicy, &flags.Helm, kubeconfig, fqdn, clusterInfos.Ingress) if err != nil { return fmt.Errorf("cannot upgrade to image %s: %s", serverImage, err) } return shared_kubernetes.WaitForDeployment(flags.Helm.Uyuni.Namespace, "uyuni", "uyuni") } 07070100000061000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/upgrade/podman07070100000062000081B400000000000000000000000165DF4690000006AA000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/upgrade/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type podmanUpgradeFlags struct { shared.UpgradeFlags `mapstructure:",squash"` Podman podman.PodmanFlags MirrorPath string } // NewCommand to upgrade a podman server. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { upgradeCmd := &cobra.Command{ Use: "podman", Short: "upgrade a local server on podman", Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { var flags podmanUpgradeFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, upgradePodman) }, } listCmd := &cobra.Command{ Use: "list", Short: "list available tag for an image", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { viper, _ := utils.ReadConfig(globalFlags.ConfigPath, cmd) var flags podmanUpgradeFlags if err := viper.Unmarshal(&flags); err != nil { log.Fatal().Err(err).Msg("Failed to unmarshall configuration") } tags, _ := podman.ShowAvailableTag(flags.Image.Name) log.Info().Msgf("Available Tags for image: %s", flags.Image.Name) for _, value := range tags { log.Info().Msgf("%s", value) } }, } shared.AddUpgradeListFlags(listCmd) upgradeCmd.AddCommand(listCmd) shared.AddUpgradeFlags(upgradeCmd) podman.AddPodmanInstallFlag(upgradeCmd) return upgradeCmd } 07070100000063000081B400000000000000000000000165DF469000000AC7000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/upgrade/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/inspect" upgrade_shared "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/shared" "github.com/uyuni-project/uyuni-tools/mgradm/shared/podman" "github.com/uyuni-project/uyuni-tools/shared" shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func upgradePodman(globalFlags *types.GlobalFlags, flags *podmanUpgradeFlags, cmd *cobra.Command, args []string) error { serverImage, err := utils.ComputeImage(flags.Image.Name, flags.Image.Tag) if err != nil { return fmt.Errorf("failed to compute image URL") } inspectedValues, err := inspect.InspectPodman(serverImage, flags.Image.PullPolicy) if err != nil { return fmt.Errorf("cannot inspect podman values: %s", err) } cnx := shared.NewConnection("podman", shared_podman.ServerContainerName, "") if err := upgrade_shared.SanityCheck(cnx, inspectedValues, serverImage); err != nil { return err } if err := shared_podman.StopService(shared_podman.ServerService); err != nil { return fmt.Errorf("cannot stop service %s", err) } defer func() { err = shared_podman.StartService(shared_podman.ServerService) }() if inspectedValues["image_pg_version"] > inspectedValues["current_pg_version"] { if err := podman.RunPgsqlVersionUpgrade(flags.Image, flags.MigrationImage, inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]); err != nil { return fmt.Errorf("cannot run PostgreSQL version upgrade script: %s", err) } } else if inspectedValues["image_pg_version"] == inspectedValues["current_pg_version"] { log.Info().Msgf("Upgrading to %s without changing PostgreSQL version", inspectedValues["uyuni_release"]) } else { return fmt.Errorf("trying to downgrade postgresql from %s to %s", inspectedValues["current_pg_version"], inspectedValues["image_pg_version"]) } schemaUpdateRequired := inspectedValues["current_pg_version"] != inspectedValues["image_pg_version"] if err := podman.RunPgsqlFinalizeScript(serverImage, schemaUpdateRequired); err != nil { return fmt.Errorf("cannot run post upgrade script: %s", err) } if err := podman.RunPostUpgradeScript(serverImage); err != nil { return fmt.Errorf("cannot run post upgrade script: %s", err) } if err := shared_podman.GenerateSystemdConfFile("uyuni-server", "Service", "Environment=UYUNI_IMAGE="+serverImage); err != nil { return err } log.Info().Msg("Waiting for the server to start...") return shared_podman.ReloadDaemon(false) } 07070100000064000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgradm/cmd/upgrade/shared07070100000065000081B400000000000000000000000165DF4690000002EF000000000000000000000000000000000000002F00000000uyuni-tools/mgradm/cmd/upgrade/shared/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/types" ) // UpgradeFlags represents flags used for upgrading a server. type UpgradeFlags struct { Image types.ImageFlags `mapstructure:",squash"` MigrationImage types.ImageFlags `mapstructure:"migration"` } // AddUpgradeFlags add upgrade flags to a command. func AddUpgradeFlags(cmd *cobra.Command) { utils.AddImageFlag(cmd) utils.AddMigrationImageFlag(cmd) } // AddUpgradeListFlags add upgrade list flags to a command. func AddUpgradeListFlags(cmd *cobra.Command) { utils.AddImageFlag(cmd) } 07070100000066000081B400000000000000000000000165DF469000001084000000000000000000000000000000000000003000000000uyuni-tools/mgradm/cmd/upgrade/shared/shared.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "errors" "fmt" "regexp" "strconv" "strings" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared" ) // CompareVersion compare the server image version and the server deployed version. func CompareVersion(imageVersion string, deployedVersion string) int { re := regexp.MustCompile(`\((.*?)\)`) imageVersionCleaned := strings.ReplaceAll(imageVersion, ".", "") imageVersionCleaned = strings.TrimSpace(imageVersionCleaned) imageVersionCleaned = re.ReplaceAllString(imageVersionCleaned, "") imageVersionInt, _ := strconv.Atoi(imageVersionCleaned) deployedVersionCleaned := strings.ReplaceAll(deployedVersion, ".", "") deployedVersionCleaned = strings.TrimSpace(deployedVersionCleaned) deployedVersionCleaned = re.ReplaceAllString(deployedVersionCleaned, "") deployedVersionInt, _ := strconv.Atoi(deployedVersionCleaned) return imageVersionInt - deployedVersionInt } func isUyuni(cnx *shared.Connection) (bool, error) { cnx_args := []string{"/etc/uyuni-release"} _, err := cnx.Exec("cat", cnx_args...) if err != nil { cnx_args := []string{"/etc/susemanager-release"} _, err := cnx.Exec("cat", cnx_args...) if err != nil { return false, errors.New("cannot find neither /etc/uyuni-release nor /etc/susemanagere-release") } return false, nil } return true, nil } // SanityCheck verifies if an upgrade can be run. func SanityCheck(cnx *shared.Connection, inspectedValues map[string]string, serverImage string) error { isUyuni, err := isUyuni(cnx) if err != nil { return fmt.Errorf("cannot check server release: %s", err) } _, isCurrentUyuni := inspectedValues["uyuni_release"] _, isCurrentSuma := inspectedValues["suse_manager_release"] if isUyuni && isCurrentSuma { return fmt.Errorf("currently SUSE Manager %s is installed, instead the image is Uyuni. Upgrade is not supported", inspectedValues["suse_manager_release"]) } if !isUyuni && isCurrentUyuni { return fmt.Errorf("currently Uyuni %s is installed, instead the image is SUSE Manager. Upgrade is not supported", inspectedValues["uyuni_release"]) } if isUyuni { cnx_args := []string{"s/Uyuni release //g", "/etc/uyuni-release"} current_uyuni_release, err := cnx.Exec("sed", cnx_args...) if err != nil { return fmt.Errorf("failed to read current uyuni release: %s", err) } log.Debug().Msgf("Current release is %s", string(current_uyuni_release)) if (len(inspectedValues["uyuni_release"])) <= 0 { return fmt.Errorf("cannot fetch release from image %s", serverImage) } log.Debug().Msgf("Image %s is %s", serverImage, inspectedValues["uyuni_release"]) if CompareVersion(inspectedValues["uyuni_release"], string(current_uyuni_release)) < 0 { return fmt.Errorf("cannot downgrade from version %s to %s", string(current_uyuni_release), inspectedValues["uyuni_release"]) } } else { cnx_args := []string{"s/SUSE Manager release //g", "/etc/susemanager-release"} current_suse_manager_release, err := cnx.Exec("sed", cnx_args...) if err != nil { return fmt.Errorf("failed to read current susemanager release: %s", err) } log.Debug().Msgf("Current release is %s", string(current_suse_manager_release)) if (len(inspectedValues["suse_manager_release"])) <= 0 { return fmt.Errorf("cannot fetch release from image %s", serverImage) } log.Debug().Msgf("Image %s is %s", serverImage, inspectedValues["suse_manager_release"]) if CompareVersion(inspectedValues["suse_manager_release"], string(current_suse_manager_release)) < 0 { return fmt.Errorf("cannot downgrade from version %s to %s", string(current_suse_manager_release), inspectedValues["suse_manager_release"]) } } if (len(inspectedValues["image_pg_version"])) <= 0 { return fmt.Errorf("cannot fetch postgresql version from %s", serverImage) } log.Debug().Msgf("Image %s has PostgreSQL %s", serverImage, inspectedValues["image_pg_version"]) if (len(inspectedValues["current_pg_version"])) <= 0 { return fmt.Errorf("posgresql is not installed in the current deployment") } log.Debug().Msgf("Current deployment has PostgreSQL %s", inspectedValues["current_pg_version"]) return nil } 07070100000067000081B400000000000000000000000165DF4690000002FC000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/cmd/upgrade/upgrade.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package upgrade import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/kubernetes" "github.com/uyuni-project/uyuni-tools/mgradm/cmd/upgrade/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand for upgrading a local server. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { upgradeCmd := &cobra.Command{ Use: "upgrade server", Short: "upgrade local server", Long: "Upgrade local server", } upgradeCmd.AddCommand(podman.NewCommand(globalFlags)) if kubernetesCmd := kubernetes.NewCommand(globalFlags); kubernetesCmd != nil { upgradeCmd.AddCommand(kubernetesCmd) } return upgradeCmd } 07070100000068000081B400000000000000000000000165DF469000000181000000000000000000000000000000000000001B00000000uyuni-tools/mgradm/main.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package main import ( "os" "github.com/uyuni-project/uyuni-tools/mgradm/cmd" ) // Run runs the `mgradm` root command. func Run() error { run, err := cmd.NewUyuniadmCommand() if err != nil { return err } return run.Execute() } func main() { if err := Run(); err != nil { os.Exit(1) } } 07070100000069000041FD00000000000000000000000765DF469000000000000000000000000000000000000000000000001A00000000uyuni-tools/mgradm/shared0707010000006A000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002500000000uyuni-tools/mgradm/shared/kubernetes0707010000006B000081B400000000000000000000000165DF4690000017A0000000000000000000000000000000000000003500000000uyuni-tools/mgradm/shared/kubernetes/certificates.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "encoding/base64" "fmt" "os" "path/filepath" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl" "github.com/uyuni-project/uyuni-tools/mgradm/shared/templates" cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func installTlsSecret(namespace string, serverCrt []byte, serverKey []byte, rootCaCrt []byte) { crdsDir, err := os.MkdirTemp("", "mgradm-*") if err != nil { log.Fatal().Err(err).Msgf("Failed to create temporary directory") } defer os.RemoveAll(crdsDir) secretPath := filepath.Join(crdsDir, "secret.yaml") log.Info().Msg("Creating SSL server certificate secret") tlsSecretData := templates.TlsSecretTemplateData{ Namespace: namespace, Name: "uyuni-cert", Certificate: base64.StdEncoding.EncodeToString(serverCrt), Key: base64.StdEncoding.EncodeToString(serverKey), RootCa: base64.StdEncoding.EncodeToString(rootCaCrt), } if err = utils.WriteTemplateToFile(tlsSecretData, secretPath, 0500, true); err != nil { log.Fatal().Err(err).Msg("Failed to generate uyuni-crt secret definition") } err = utils.RunCmd("kubectl", "apply", "-f", secretPath) if err != nil { log.Fatal().Err(err).Msg("Failed to create uyuni-crt TLS secret") } createCaConfig(rootCaCrt) } // Install cert-manager and its CRDs using helm in the cert-manager namespace if needed // and then create a self-signed CA and issuers. // Returns helm arguments to be added to use the issuer. func installSslIssuers(helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, rootCa string, tlsCert *ssl.SslPair, kubeconfig, fqdn string, imagePullPolicy string) ([]string, error) { // Install cert-manager if needed if err := installCertManager(helmFlags, kubeconfig, imagePullPolicy); err != nil { return []string{}, fmt.Errorf("cannot install cert manager: %s", err) } log.Info().Msg("Creating SSL certificate issuer") crdsDir, err := os.MkdirTemp("", "mgradm-*") if err != nil { return []string{}, fmt.Errorf("failed to create temporary directory: %s", err) } defer os.RemoveAll(crdsDir) issuerPath := filepath.Join(crdsDir, "issuer.yaml") issuerData := templates.IssuerTemplateData{ Namespace: helmFlags.Uyuni.Namespace, Country: sslFlags.Country, State: sslFlags.State, City: sslFlags.City, Org: sslFlags.Org, OrgUnit: sslFlags.OU, Email: sslFlags.Email, Fqdn: fqdn, RootCa: rootCa, Key: tlsCert.Key, Certificate: tlsCert.Cert, } if err = utils.WriteTemplateToFile(issuerData, issuerPath, 0500, true); err != nil { return []string{}, fmt.Errorf("failed to generate issuer definition: %s", err) } err = utils.RunCmd("kubectl", "apply", "-f", issuerPath) if err != nil { log.Fatal().Err(err).Msg("Failed to create issuer") } // Wait for issuer to be ready for i := 0; i < 60; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "-o=jsonpath={.status.conditions[*].type}", "issuer", "uyuni-ca-issuer") if err == nil && string(out) == "Ready" { return []string{"--set-json", "ingressSslAnnotations={\"cert-manager.io/issuer\": \"uyuni-ca-issuer\"}"}, nil } time.Sleep(1 * time.Second) } log.Fatal().Msg("Issuer didn't turn ready after 60s") return []string{}, nil } func installCertManager(helmFlags *cmd_utils.HelmFlags, kubeconfig string, imagePullPolicy string) error { if !kubernetes.IsDeploymentReady("", "cert-manager") { log.Info().Msg("Installing cert-manager") repo := "" chart := helmFlags.CertManager.Chart version := helmFlags.CertManager.Version namespace := helmFlags.CertManager.Namespace args := []string{ "--set", "installCRDs=true", "--set-json", "global.commonLabels={\"installedby\": \"mgradm\"}", "--set", "images.pullPolicy=" + kubernetes.GetPullPolicy(imagePullPolicy), } extraValues := helmFlags.CertManager.Values if extraValues != "" { args = append(args, "-f", extraValues) } // Use upstream chart if nothing defined if chart == "" { repo = "https://charts.jetstack.io" chart = "cert-manager" } // The installedby label will be used to only uninstall what we installed if err := kubernetes.HelmUpgrade(kubeconfig, namespace, true, repo, "cert-manager", chart, version, args...); err != nil { return fmt.Errorf("cannot run helm upgrade: %s", err) } } // Wait for cert-manager to be ready err := kubernetes.WaitForDeployment("", "cert-manager-webhook", "webhook") if err != nil { return fmt.Errorf("cannot deploy: %s", err) } return nil } func extractCaCertToConfig() { // TODO Replace with [trust-manager](https://cert-manager.io/docs/projects/trust-manager/) to automate this const jsonPath = "-o=jsonpath={.data.ca\\.crt}" log.Info().Msg("Extracting CA certificate to a configmap") // Skip extracting if the configmap is already present out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "configmap", "uyuni-ca", jsonPath) log.Info().Msgf("CA cert: %s", string(out)) if err == nil && len(out) > 0 { log.Info().Msg("uyuni-ca configmap already existing, skipping extraction") return } out, err = utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "secret", "uyuni-ca", jsonPath) if err != nil { log.Fatal().Err(err).Msgf("Failed to get uyuni-ca certificate") } decoded, err := base64.StdEncoding.DecodeString(string(out)) if err != nil { log.Fatal().Err(err).Msgf("Failed to base64 decode CA certificate") } createCaConfig(decoded) } func createCaConfig(ca []byte) { valueArg := "--from-literal=ca.crt=" + string(ca) if err := utils.RunCmd("kubectl", "create", "configmap", "uyuni-ca", valueArg); err != nil { log.Fatal().Err(err).Msg("Failed to create uyuni-ca config map from certificate") } } 0707010000006C000081B400000000000000000000000165DF4690000010F2000000000000000000000000000000000000003000000000uyuni-tools/mgradm/shared/kubernetes/install.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "fmt" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl" cmd_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // HELM_APP_NAME is the Helm application name. const HELM_APP_NAME = "uyuni" // Deploy execute a deploy of a given image and helm to a cluster. func Deploy(cnx *shared.Connection, imageFlags *types.ImageFlags, helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, clusterInfos *kubernetes.ClusterInfos, fqdn string, debug bool, helmArgs ...string) error { // If installing on k3s, install the traefik helm config in manifests isK3s := clusterInfos.IsK3s() IsRke2 := clusterInfos.IsRke2() if isK3s { InstallK3sTraefikConfig(debug) } else if IsRke2 { kubernetes.InstallRke2NginxConfig(utils.TCP_PORTS, utils.UDP_PORTS, helmFlags.Uyuni.Namespace) } serverImage, err := utils.ComputeImage(imageFlags.Name, imageFlags.Tag) if err != nil { return fmt.Errorf("failed to compute image URL") } // Install the uyuni server helm chart err = UyuniUpgrade(serverImage, imageFlags.PullPolicy, helmFlags, clusterInfos.GetKubeconfig(), fqdn, clusterInfos.Ingress, helmArgs...) if err != nil { return fmt.Errorf("cannot upgrade: %s", err) } // Wait for the pod to be started err = kubernetes.WaitForDeployment(helmFlags.Uyuni.Namespace, HELM_APP_NAME, "uyuni") if err != nil { return fmt.Errorf("cannot deploy: %s", err) } return cnx.WaitForServer() } // DeployCertificate executre a deploy a new certificate given an helm. func DeployCertificate(helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, rootCa string, ca *ssl.SslPair, kubeconfig string, fqdn string, imagePullPolicy string) ([]string, error) { helmArgs := []string{} if sslFlags.UseExisting() { DeployExistingCertificate(helmFlags, sslFlags, kubeconfig) } else { // Install cert-manager and a self-signed issuer ready for use issuerArgs, err := installSslIssuers(helmFlags, sslFlags, rootCa, ca, kubeconfig, fqdn, imagePullPolicy) if err != nil { return []string{}, fmt.Errorf("cannot install cert-manager and self-sign issuer: %s", err) } helmArgs = append(helmArgs, issuerArgs...) // Extract the CA cert into uyuni-ca config map as the container shouldn't have the CA secret extractCaCertToConfig() } return helmArgs, nil } // DeployExistingCertificate execute a deploy of an existing certificate. func DeployExistingCertificate(helmFlags *cmd_utils.HelmFlags, sslFlags *cmd_utils.SslCertFlags, kubeconfig string) { // Deploy the SSL Certificate secret and CA configmap serverCrt, rootCaCrt := ssl.OrderCas(&sslFlags.Ca, &sslFlags.Server) serverKey := utils.ReadFile(sslFlags.Server.Key) installTlsSecret(helmFlags.Uyuni.Namespace, serverCrt, serverKey, rootCaCrt) // Extract the CA cert into uyuni-ca config map as the container shouldn't have the CA secret extractCaCertToConfig() } // UyuniUpgrade runs an helm upgrade using images and helm configuration as parameters. func UyuniUpgrade(serverImage string, pullPolicy string, helmFlags *cmd_utils.HelmFlags, kubeconfig string, fqdn string, ingress string, helmArgs ...string) error { log.Info().Msg("Installing Uyuni") // The guessed ingress is passed before the user's value to let the user override it in case we got it wrong. helmParams := []string{ "--set", "ingress=" + ingress, } extraValues := helmFlags.Uyuni.Values if extraValues != "" { helmParams = append(helmParams, "-f", extraValues) } // The values computed from the command line need to be last to override what could be in the extras helmParams = append(helmParams, "--set", "images.server="+serverImage, "--set", "pullPolicy="+kubernetes.GetPullPolicy(pullPolicy), "--set", "fqdn="+fqdn) helmParams = append(helmParams, helmArgs...) namespace := helmFlags.Uyuni.Namespace chart := helmFlags.Uyuni.Chart version := helmFlags.Uyuni.Version return kubernetes.HelmUpgrade(kubeconfig, namespace, true, "", HELM_APP_NAME, chart, version, helmParams...) } 0707010000006D000081B400000000000000000000000165DF469000001C32000000000000000000000000000000000000002C00000000uyuni-tools/mgradm/shared/kubernetes/k3s.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "fmt" "os" "github.com/rs/zerolog/log" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // InstallK3sTraefikConfig installs the K3s Traefik configuration. func InstallK3sTraefikConfig(debug bool) { tcpPorts := []types.PortMap{} tcpPorts = append(tcpPorts, utils.TCP_PORTS...) if debug { tcpPorts = append(tcpPorts, utils.DEBUG_PORTS...) } kubernetes.InstallK3sTraefikConfig(tcpPorts, utils.UDP_PORTS) } // RunPgsqlVersionUpgrade perform a PostgreSQL major upgrade. func RunPgsqlVersionUpgrade(image types.ImageFlags, migrationImage types.ImageFlags, nodeName string, oldPgsql string, newPgsql string) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } if newPgsql > oldPgsql { log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", oldPgsql, newPgsql) pgsqlVersionUpgradeContainer := "uyuni-upgrade-pgsql" migrationImageUrl := "" if migrationImage.Name == "" { migrationImageUrl, err = utils.ComputeImage(image.Name, image.Tag, fmt.Sprintf("-migration-%s-%s", oldPgsql, newPgsql)) if err != nil { return fmt.Errorf("failed to compute image URL %s", err) } } else { migrationImageUrl, err = utils.ComputeImage(migrationImage.Name, image.Tag) if err != nil { return fmt.Errorf("failed to compute image URL %s", err) } } log.Info().Msgf("Using migration image %s", migrationImageUrl) pgsqlVersionUpgradeScriptName, err := adm_utils.GeneratePgsqlVersionUpgradeScript(scriptDir, oldPgsql, newPgsql, true) if err != nil { return fmt.Errorf("cannot generate postgresql database version upgrade script: %s", err) } //delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong if err := kubernetes.DeletePod(pgsqlVersionUpgradeContainer, kubernetes.ServerFilter); err != nil { return fmt.Errorf("cannot delete %s: %s", pgsqlVersionUpgradeContainer, err) } //generate deploy data pgsqlVersioUpgradeDeployData := types.Deployment{ APIVersion: "v1", Spec: &types.Spec{ RestartPolicy: "Never", NodeName: nodeName, Containers: []types.Container{ { Name: pgsqlVersionUpgradeContainer, VolumeMounts: append(utils.PgsqlRequiredVolumeMounts, types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}), }, }, Volumes: append(utils.PgsqlRequiredVolumes, types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}), }, } //transform deploy in JSON overridePgsqlVersioUpgrade, err := kubernetes.GenerateOverrideDeployment(pgsqlVersioUpgradeDeployData) if err != nil { return err } err = kubernetes.RunPod(pgsqlVersionUpgradeContainer, kubernetes.ServerFilter, migrationImageUrl, image.PullPolicy, "/var/lib/uyuni-tools/"+pgsqlVersionUpgradeScriptName, overridePgsqlVersioUpgrade) if err != nil { return fmt.Errorf("error running container %s: %s", pgsqlVersionUpgradeContainer, err) } } return nil } // RunPgsqlFinalizeScript run the script with all the action required to a db after upgrade. func RunPgsqlFinalizeScript(serverImage string, pullPolicy string, nodeName string, schemaUpdateRequired bool) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } pgsqlFinalizeContainer := "uyuni-finalize-pgsql" pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, true, true) if err != nil { return fmt.Errorf("cannot generate psql finalize script: %s", err) } //delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong if err := kubernetes.DeletePod(pgsqlFinalizeContainer, kubernetes.ServerFilter); err != nil { return fmt.Errorf("cannot delete %s: %s", pgsqlFinalizeContainer, err) } //generate deploy data pgsqlFinalizeDeployData := types.Deployment{ APIVersion: "v1", Spec: &types.Spec{ RestartPolicy: "Never", NodeName: nodeName, Containers: []types.Container{ { Name: pgsqlFinalizeContainer, VolumeMounts: append(utils.PgsqlRequiredVolumeMounts, types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}), }, }, Volumes: append(utils.PgsqlRequiredVolumes, types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}), }, } //transform deploy data in JSON overridePgsqlFinalize, err := kubernetes.GenerateOverrideDeployment(pgsqlFinalizeDeployData) if err != nil { return err } err = kubernetes.RunPod(pgsqlFinalizeContainer, kubernetes.ServerFilter, serverImage, pullPolicy, "/var/lib/uyuni-tools/"+pgsqlFinalizeScriptName, overridePgsqlFinalize) if err != nil { return fmt.Errorf("error running container %s: %s", pgsqlFinalizeContainer, err) } return nil } // RunPostUpgradeScript run the script with the changes to apply after the upgrade. func RunPostUpgradeScript(serverImage string, pullPolicy string, nodeName string) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } postUpgradeContainer := "uyuni-post-upgrade" postUpgradeScriptName, err := adm_utils.GeneratePostUpgradeScript(scriptDir, "localhost") if err != nil { return fmt.Errorf("cannot generate postgresql finalization script %s", err) } //delete pending pod and then check the node, because in presence of more than a pod GetNode return is wrong if err := kubernetes.DeletePod(postUpgradeContainer, kubernetes.ServerFilter); err != nil { return fmt.Errorf("cannot delete %s: %s", postUpgradeContainer, err) } //generate deploy data postUpgradeDeployData := types.Deployment{ APIVersion: "v1", Spec: &types.Spec{ RestartPolicy: "Never", NodeName: nodeName, Containers: []types.Container{ { Name: postUpgradeContainer, VolumeMounts: append(utils.EtcServerVolumeMounts, types.VolumeMount{MountPath: "/var/lib/uyuni-tools", Name: "var-lib-uyuni-tools"}), }, }, Volumes: append(utils.EtcServerVolumes, types.Volume{Name: "var-lib-uyuni-tools", HostPath: &types.HostPath{Path: scriptDir, Type: "Directory"}}), }, } //transform deploy data in JSON overridePostUpgrade, err := kubernetes.GenerateOverrideDeployment(postUpgradeDeployData) if err != nil { return err } err = kubernetes.RunPod(postUpgradeContainer, kubernetes.ServerFilter, serverImage, pullPolicy, "/var/lib/uyuni-tools/"+postUpgradeScriptName, overridePostUpgrade) if err != nil { return fmt.Errorf("error running container %s: %s", postUpgradeContainer, err) } return nil } 0707010000006E000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002100000000uyuni-tools/mgradm/shared/podman0707010000006F000081B400000000000000000000000165DF4690000027D8000000000000000000000000000000000000002B00000000uyuni-tools/mgradm/shared/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os" "path" "path/filepath" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl" "github.com/uyuni-project/uyuni-tools/mgradm/shared/templates" adm_utils "github.com/uyuni-project/uyuni-tools/mgradm/shared/utils" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) const commonArgs = "--rm --cap-add NET_RAW --tmpfs /run -v cgroup:/sys/fs/cgroup:rw" // GetCommonParams splits the common arguments. func GetCommonParams() []string { return strings.Split(commonArgs, " ") } // GetExposedPorts returns the port exposed. func GetExposedPorts(debug bool) []types.PortMap { ports := []types.PortMap{ utils.NewPortMap("https", 443, 443), utils.NewPortMap("http", 80, 80), } ports = append(ports, utils.TCP_PORTS...) ports = append(ports, utils.UDP_PORTS...) if debug { ports = append(ports, utils.DEBUG_PORTS...) } return ports } // GenerateSystemdService creates a serverY systemd file. func GenerateSystemdService(tz string, image string, debug bool, podmanArgs []string) error { if err := podman.SetupNetwork(); err != nil { return fmt.Errorf("cannot setup network: %s", err) } log.Info().Msg("Enabling system service") data := templates.PodmanServiceTemplateData{ Volumes: utils.ServerVolumeMounts, NamePrefix: "uyuni", Args: commonArgs + " " + strings.Join(podmanArgs, " "), Ports: GetExposedPorts(debug), Timezone: tz, Network: podman.UyuniNetwork, } if err := utils.WriteTemplateToFile(data, podman.GetServicePath("uyuni-server"), 0555, false); err != nil { return fmt.Errorf("failed to generate systemd service unit file: %s", err) } if err := podman.GenerateSystemdConfFile("uyuni-server", "Service", "Environment=UYUNI_IMAGE="+image); err != nil { return fmt.Errorf("cannot generate systemd conf file: %s", err) } return podman.ReloadDaemon(false) } // UpdateSslCertificate update SSL certificate. func UpdateSslCertificate(cnx *shared.Connection, chain *ssl.CaChain, serverPair *ssl.SslPair) error { ssl.CheckPaths(chain, serverPair) // Copy the CAs, certificate and key to the container const certDir = "/tmp/uyuni-tools" if err := utils.RunCmd("podman", "exec", podman.ServerContainerName, "mkdir", "-p", certDir); err != nil { return fmt.Errorf("failed to create temporary folder on container to copy certificates to") } rootCaPath := path.Join(certDir, "root-ca.crt") serverCrtPath := path.Join(certDir, "server.crt") serverKeyPath := path.Join(certDir, "server.key") log.Debug().Msgf("Intermediate CA flags: %v", chain.Intermediate) args := []string{ "exec", podman.ServerContainerName, "mgr-ssl-cert-setup", "-vvv", "--root-ca-file", rootCaPath, "--server-cert-file", serverCrtPath, "--server-key-file", serverKeyPath, } if err := cnx.Copy(chain.Root, "server:"+rootCaPath, "root", "root"); err != nil { return fmt.Errorf("cannot copy %s: %s", rootCaPath, err) } if err := cnx.Copy(serverPair.Cert, "server:"+serverCrtPath, "root", "root"); err != nil { return fmt.Errorf("cannot copy %s: %s", serverCrtPath, err) } if err := cnx.Copy(serverPair.Key, "server:"+serverKeyPath, "root", "root"); err != nil { return fmt.Errorf("cannot copy %s: %s", serverKeyPath, err) } for i, ca := range chain.Intermediate { caFilename := fmt.Sprintf("ca-%d.crt", i) caPath := path.Join(certDir, caFilename) args = append(args, "--intermediate-ca-file", caPath) if err := cnx.Copy(ca, "server:"+caPath, "root", "root"); err != nil { return fmt.Errorf("cannot copy %s: %s", caPath, err) } } // Check and install then using mgr-ssl-cert-setup if _, err := utils.RunCmdOutput(zerolog.InfoLevel, "podman", args...); err != nil { return fmt.Errorf("failed to update SSL certificate") } // Clean the copied files and the now useless ssl-build if err := utils.RunCmd("podman", "exec", podman.ServerContainerName, "rm", "-rf", certDir); err != nil { return fmt.Errorf("failed to remove copied certificate files in the container") } const sslbuildPath = "/root/ssl-build" if cnx.TestExistenceInPod(sslbuildPath) { if err := utils.RunCmd("podman", "exec", podman.ServerContainerName, "rm", "-rf", sslbuildPath); err != nil { return fmt.Errorf("failed to remove now useless ssl-build folder in the container") } } // The services need to be restarted log.Info().Msg("Restarting services after updating the certificate") return utils.RunCmdStdMapping("podman", "exec", podman.ServerContainerName, "spacewalk-service", "restart") } // RunContainer execute a container. func RunContainer(name string, image string, extraArgs []string, cmd []string) error { podmanArgs := append([]string{"run", "--name", name}, GetCommonParams()...) podmanArgs = append(podmanArgs, extraArgs...) for _, volume := range utils.ServerVolumeMounts { podmanArgs = append(podmanArgs, "-v", volume.Name+":"+volume.MountPath) } podmanArgs = append(podmanArgs, image) podmanArgs = append(podmanArgs, cmd...) err := utils.RunCmdStdMapping("podman", podmanArgs...) if err != nil { return fmt.Errorf("failed to run %s container: %s", name, err) } return nil } // RunPostUpgradeScript run the script with the changes to apply after the upgrade. func RunPostUpgradeScript(serverImage string) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } extraArgs := []string{ "-v", scriptDir + ":/var/lib/uyuni-tools/", "--security-opt", "label:disable", } postUpgradeScriptName, err := adm_utils.GeneratePostUpgradeScript(scriptDir, "localhost") if err != nil { return fmt.Errorf("cannot generate postgresql finalization script %s", err) } err = RunContainer("uyuni-post-upgrade", serverImage, extraArgs, []string{"/var/lib/uyuni-tools/" + postUpgradeScriptName}) if err != nil { return err } return nil } // RunPgsqlFinalizeScript run the script with all the action required to a db after upgrade. func RunPgsqlFinalizeScript(serverImage string, schemaUpdateRequired bool) error { scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } extraArgs := []string{ "-v", scriptDir + ":/var/lib/uyuni-tools/", "--security-opt", "label:disable", } pgsqlFinalizeScriptName, err := adm_utils.GenerateFinalizePostgresScript(scriptDir, true, schemaUpdateRequired, true, true, false) if err != nil { return fmt.Errorf("cannot generate postgresql finalization script %s", err) } err = RunContainer("uyuni-finalize-pgsql", serverImage, extraArgs, []string{"/var/lib/uyuni-tools/" + pgsqlFinalizeScriptName}) if err != nil { return err } return nil } // RunPgsqlVersionUpgrade perform a PostgreSQL major upgrade. func RunPgsqlVersionUpgrade(image types.ImageFlags, migrationImage types.ImageFlags, oldPgsql string, newPgsql string) error { log.Info().Msgf("Previous postgresql is %s, instead new one is %s. Performing a DB version upgrade...", oldPgsql, newPgsql) scriptDir, err := os.MkdirTemp("", "mgradm-*") defer os.RemoveAll(scriptDir) if err != nil { return fmt.Errorf("failed to create temporary directory") } extraArgs := []string{ "-v", scriptDir + ":/var/lib/uyuni-tools/", "--security-opt", "label:disable", } migrationImageUrl := "" if migrationImage.Name == "" { migrationImageUrl, err = utils.ComputeImage(image.Name, image.Tag, fmt.Sprintf("-migration-%s-%s", oldPgsql, newPgsql)) if err != nil { return fmt.Errorf("failed to compute image URL %s", err) } } else { migrationImageUrl, err = utils.ComputeImage(migrationImage.Name, image.Tag) if err != nil { return fmt.Errorf("failed to compute image URL %s", err) } } err = podman.PrepareImage(migrationImageUrl, image.PullPolicy) if err != nil { return err } log.Info().Msgf("Using migration image %s", migrationImageUrl) pgsqlVersionUpgradeScriptName, err := adm_utils.GeneratePgsqlVersionUpgradeScript(scriptDir, oldPgsql, newPgsql, false) if err != nil { return fmt.Errorf("cannot generate postgresql database version upgrade script %s", err) } err = RunContainer("uyuni-upgrade-pgsql", migrationImageUrl, extraArgs, []string{"/var/lib/uyuni-tools/" + pgsqlVersionUpgradeScriptName}) if err != nil { return err } return nil } // RunMigration migrate an existing remote server to a container. func RunMigration(serverImage string, pullPolicy string, sshAuthSocket string, sshConfigPath string, sshKnownhostsPath string, sourceFqdn string) (string, string, string, error) { scriptDir, err := adm_utils.GenerateMigrationScript(sourceFqdn, false) if err != nil { return "", "", "", fmt.Errorf("cannot generate migration script: %s", err) } defer os.RemoveAll(scriptDir) extraArgs := []string{ "--security-opt", "label:disable", "-e", "SSH_AUTH_SOCK", "-v", filepath.Dir(sshAuthSocket) + ":" + filepath.Dir(sshAuthSocket), "-v", scriptDir + ":/var/lib/uyuni-tools/", } if sshConfigPath != "" { extraArgs = append(extraArgs, "-v", sshConfigPath+":/tmp/ssh_config") } if sshKnownhostsPath != "" { extraArgs = append(extraArgs, "-v", sshKnownhostsPath+":/etc/ssh/ssh_known_hosts") } if err != nil { return "", "", "", fmt.Errorf("failed to compute image URL: %s", err) } err = podman.PrepareImage(serverImage, pullPolicy) if err != nil { return "", "", "", err } log.Info().Msg("Migrating server") if err := RunContainer("uyuni-migration", serverImage, extraArgs, []string{"/var/lib/uyuni-tools/migrate.sh"}); err != nil { return "", "", "", fmt.Errorf("cannot run uyuni migration container: %s", err) } tz, oldPgVersion, newPgVersion, err := adm_utils.ReadContainerData(scriptDir) if err != nil { return "", "", "", fmt.Errorf("cannot read extracted data: %s", err) } return tz, oldPgVersion, newPgVersion, nil } 07070100000070000041FD00000000000000000000000365DF469000000000000000000000000000000000000000000000001E00000000uyuni-tools/mgradm/shared/ssl07070100000071000081B400000000000000000000000165DF469000001D9C000000000000000000000000000000000000002500000000uyuni-tools/mgradm/shared/ssl/ssl.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package ssl import ( "bytes" "errors" "os" "os/exec" "strings" "time" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // CaChain is a type to store CA Chain. type CaChain struct { Root string Intermediate []string } // SslPait is a type for SSL Cert and Key. type SslPair struct { Cert string Key string } // Generate the server certificate with the CA chain. // Returns the certificate chain and the root CA. func OrderCas(chain *CaChain, serverPair *SslPair) ([]byte, []byte) { CheckPaths(chain, serverPair) // Extract all certificates and their data certs := readCertificates(chain.Root) for _, caPath := range chain.Intermediate { certs = append(certs, readCertificates(caPath)...) } serverCerts := readCertificates(serverPair.Cert) certs = append(certs, serverCerts...) serverCert, err := findServerCert(certs) if err != nil { log.Fatal().Msg("Failed to find a non-CA certificate") } // Map all certificates using their hashes mapBySubjectHash := map[string]certificate{} if serverCert.subjectHash != "" { mapBySubjectHash[serverCert.subjectHash] = *serverCert } for _, caCert := range certs { if caCert.subjectHash != "" { mapBySubjectHash[caCert.subjectHash] = caCert } } // Sort from server certificate to RootCA return sortCertificates(mapBySubjectHash, serverCert.subjectHash) } type certificate struct { content []byte subject string subjectHash string issuer string issuerHash string startDate time.Time endDate time.Time subjectKeyId string authKeyId string isCa bool isRoot bool } func findServerCert(certs []certificate) (*certificate, error) { for _, cert := range certs { if !cert.isCa { return &cert, nil } } return nil, errors.New("expected to find a certificate, got none") } func readCertificates(path string) []certificate { fd, err := os.Open(path) if err != nil { log.Fatal().Err(err).Msgf("Failed to read certificate file %s", path) } certs := []certificate{} for { log.Debug().Msgf("Running openssl x509 on %s", path) cmd := exec.Command("openssl", "x509") cmd.Stdin = fd out, err := cmd.Output() if err != nil { // openssl got an invalid certificate or the end of the file break } // Extract data from the certificate cert := extractCertificateData(out) certs = append(certs, cert) } return certs } // Extract data from the certificate to help ordering and verifying it. func extractCertificateData(content []byte) certificate { args := []string{"x509", "-noout", "-subject", "-subject_hash", "-startdate", "-enddate", "-issuer", "-issuer_hash", "-ext", "subjectKeyIdentifier,authorityKeyIdentifier,basicConstraints"} log.Debug().Msg("Running command openssl " + strings.Join(args, " ")) cmd := exec.Command("openssl", args...) log.Trace().Msgf("Extracting data from certificate:\n%s", string(content)) reader := bytes.NewReader(content) cmd.Stdin = reader out, err := cmd.Output() if err != nil { log.Fatal().Err(err).Msg("Failed to extract data from certificate") } lines := strings.Split(string(out), "\n") cert := certificate{content: content} const timeLayout = "Jan 2 15:04:05 2006 MST" nextVal := "" for _, line := range lines { if strings.TrimSpace(line) == "" { continue } if strings.HasPrefix(line, "subject=") { cert.subject = strings.SplitN(line, "=", 2)[1] } else if strings.HasPrefix(line, "issuer=") { cert.issuer = strings.SplitN(line, "=", 2)[1] } else if strings.HasPrefix(line, "notBefore=") { date := strings.SplitN(line, "=", 2)[1] cert.startDate, err = time.Parse(timeLayout, date) if err != nil { log.Fatal().Err(err).Msgf("Failed to parse start date: %s\n", date) } } else if strings.HasPrefix(line, "notAfter=") { date := strings.SplitN(line, "=", 2)[1] cert.endDate, err = time.Parse(timeLayout, date) if err != nil { log.Fatal().Err(err).Msgf("Failed to parse end date: %s\n", date) } } else if strings.HasPrefix(line, "X509v3 Subject Key Identifier") { nextVal = "subjectKeyId" } else if strings.HasPrefix(line, "X509v3 Authority Key Identifier") { nextVal = "authKeyId" } else if strings.HasPrefix(line, "X509v3 Basic Constraints") { nextVal = "basicConstraints" } else if strings.HasPrefix(line, " ") { if nextVal == "subjectKeyId" { cert.subjectKeyId = strings.ToUpper(strings.TrimSpace(line)) } else if nextVal == "authKeyId" && strings.HasPrefix(line, " keyid:") { cert.authKeyId = strings.ToUpper(strings.TrimSpace(strings.SplitN(line, ":", 2)[1])) } else if nextVal == "basicConstraints" && strings.Contains(line, "CA:TRUE") { cert.isCa = true } else { // Unhandled extension value continue } } else if cert.subjectHash == "" { // subject_hash comes first without key to identify it cert.subjectHash = strings.TrimSpace(line) } else { // second issue_hash without key to identify this value cert.issuerHash = strings.TrimSpace(line) } } if cert.subject == cert.issuer { cert.isRoot = true // Some Root CAs might not have their authorityKeyIdentifier set to themself if cert.isCa && cert.authKeyId == "" { cert.authKeyId = cert.subjectKeyId } } else { cert.isRoot = false } return cert } // Prepare the certificate chain starting by the server up to the root CA. // Returns the certificate chain and the root CA. func sortCertificates(mapBySubjectHash map[string]certificate, serverCertHash string) ([]byte, []byte) { if len(mapBySubjectHash) == 0 { log.Fatal().Msg("No CA found in hash") } cert := mapBySubjectHash[serverCertHash] issuerHash := cert.issuerHash _, found := mapBySubjectHash[issuerHash] if issuerHash == "" || !found { log.Fatal().Msg("No CA found for server certificate") } sortedChain := bytes.NewBuffer(mapBySubjectHash[serverCertHash].content) var rootCa []byte for { cert, found = mapBySubjectHash[issuerHash] if !found { log.Fatal().Msgf("Missing CA with subject hash %s", issuerHash) } nextHash := cert.issuerHash if nextHash == issuerHash { // Found Root CA, we can exit rootCa = cert.content break } issuerHash = nextHash sortedChain.Write(cert.content) } return sortedChain.Bytes(), rootCa } // Ensures that all the passed path exists and the required files are available. func CheckPaths(chain *CaChain, serverPair *SslPair) { mandatoryFile(chain.Root, "root CA") for _, ca := range chain.Intermediate { optionalFile(ca) } mandatoryFile(serverPair.Cert, "server certificate") mandatoryFile(serverPair.Key, "server key") } func mandatoryFile(file string, msg string) { if file == "" { log.Fatal().Msgf("%s is required", msg) } optionalFile(file) } func optionalFile(file string) { if file != "" && !utils.FileExists(file) { log.Fatal().Msgf("%s file is not accessible", file) } } // Converts an SSL key to RSA. func GetRsaKey(keyPath string, password string) []byte { // Kubernetes only handles RSA private TLS keys, convert and strip password caPassword := password utils.AskIfMissing(&caPassword, "Source server SSL CA private key password") // Convert the key file to RSA format for kubectl to handle it cmd := exec.Command("openssl", "rsa", "-in", keyPath, "-passin", "env:pass") cmd.Env = append(cmd.Env, "pass="+caPassword) out, err := cmd.Output() if err != nil { log.Fatal().Err(err).Msg("Failed to convert CA private key to RSA") } return out } 07070100000072000081B400000000000000000000000165DF4690000016C7000000000000000000000000000000000000002A00000000uyuni-tools/mgradm/shared/ssl/ssl_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package ssl import ( "strings" "testing" ) func TestReadCertificatesRootCa(t *testing.T) { actual := readCertificates("testdata/chain1/root-ca.crt") if len(actual) != 1 { t.Errorf("readCertificates got %d certificates; want 1", len(actual)) } if !actual[0].isRoot { t.Error("CA should be root") } } func TestReadCertificatesNoCa(t *testing.T) { actual := readCertificates("testdata/chain1/server.crt") if len(actual) != 1 { t.Errorf("readCertificates got %d certificates; want 1", len(actual)) } if actual[0].isCa { t.Error("Shouldn't be a CA certificate") } } func TestReadCertificatesMultiple(t *testing.T) { actual := readCertificates("testdata/chain1/intermediate-ca.crt") if len(actual) != 2 { t.Errorf("readCertificates got %d certificates; want 2", len(actual)) } content := string(actual[0].content) if !strings.HasPrefix(content, "-----BEGIN CERTIFICATE-----\nMIIEXjCCA0agA") || !strings.HasSuffix(content, "nrUN5m7Y0taw4qrOVOZRmGXu\n-----END CERTIFICATE-----\n") { t.Errorf("Wrong certificate content:\n%s", content) } if actual[1].subject != "C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = TeamCA" { t.Errorf("Wrong certificate subject: %s", actual[1].subject) } if actual[1].subjectHash != "85a51924" { t.Errorf("Wrong subject hash: %s", actual[1].subjectHash) } if actual[0].issuer != "C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA" { t.Errorf("Wrong certificate issuer: %s", actual[0].issuer) } if actual[0].issuerHash != "e96ab651" { t.Errorf("Wrong issuer hash: %s", actual[0].issuerHash) } if actual[0].isRoot { t.Error("CA shouldn't be root") } if !actual[0].isCa { t.Error("Should be a CA") } if actual[1].subjectKeyId != "62:00:25:E4:EE:70:E5:37:2D:1E:9E:AE:4E:B7:3E:FC:62:08:BF:27" { t.Errorf("Wrong subject key id: %s", actual[1].subjectKeyId) } if actual[0].authKeyId != "6E:6D:4B:35:22:23:3E:13:18:A5:93:61:0E:9C:BE:1E:D2:B8:1B:D4" { t.Errorf("Wrong auth key id: %s", actual[0].authKeyId) } } func TestOrderCas(t *testing.T) { chain := CaChain{Root: "testdata/chain1/root-ca.crt", Intermediate: []string{"testdata/chain1/intermediate-ca.crt"}} server := SslPair{Cert: "testdata/chain1/server.crt", Key: "testdata/chain1/server.key"} certs, rootCa := OrderCas(&chain, &server) ordered := strings.Split(string(certs), "-----BEGIN CERTIFICATE-----\n") if ordered[0] != "" { t.Errorf("Found unknown content before first certificate: %s", ordered[0]) } onlyCerts := ordered[1:] expected := []struct { Begin string End string }{ {Begin: "MIIEdDCCA1ygAwIBAgIUZ2P1Ka9Eun", End: "JtS8rmkQpYyJciifX0PxYzTg=="}, {Begin: "MIIETzCCAzegAwIBAgIUZ2P1Ka9Eun", End: "s3DjcCbkzyTUCKh9Po4\nmoUf"}, {Begin: "MIIEXjCCA0agAwIBAgIUZ2P1Ka9Eunnv3dy/", End: "nrUN5m7Y0taw4qrOVOZRmGXu"}, } // Do not count the empty first item if len(onlyCerts) != len(expected) { t.Errorf("Wrong number of certificates in the chain: got %d; want %d", len(onlyCerts), len(expected)) } for i, data := range expected { if !strings.HasPrefix(onlyCerts[i], data.Begin) || !strings.HasSuffix(onlyCerts[i], data.End+"\n-----END CERTIFICATE-----\n") { t.Errorf("Invalid certificate #%d, got:\n:%s", i, onlyCerts[i]) } } rootCert := string(rootCa) if !strings.HasPrefix(rootCert, "-----BEGIN CERTIFICATE-----\nMIIEVjCCAz6gAwIBAgIUSZYESIXLDe") || !strings.HasSuffix(rootCert, "5c7cfxV\nkABuj9PJxnNnFQ==\n-----END CERTIFICATE-----\n") { t.Errorf("Invalid root CA certificate, got:\n:%s", rootCert) } } func TestFindServerCertificate(t *testing.T) { certsList := readCertificates("testdata/chain2/spacewalk.crt") actual, err := findServerCert(certsList) if err != nil { t.Error("Expected to find a certificate, got none") } if actual.subjectHash != "78b716a6" { t.Errorf("Wrong subject hash, got %s", actual.subjectHash) } } // Test a CA chain with all the chain in the server certificate file. func TestOrderCasChain2(t *testing.T) { chain := CaChain{Root: "testdata/chain2/RHN-ORG-TRUSTED-SSL-CERT", Intermediate: []string{}} server := SslPair{Cert: "testdata/chain2/spacewalk.crt", Key: "testdata/chain2/spacewalk.key"} certs, rootCa := OrderCas(&chain, &server) ordered := strings.Split(string(certs), "-----BEGIN CERTIFICATE-----\n") if ordered[0] != "" { t.Errorf("Found unknown content before first certificate: %s", ordered[0]) } onlyCerts := ordered[1:] expected := []struct { Begin string End string }{ {Begin: "MIIEejCCA2KgAwIBAgIUEbWzxg57E", End: "Ur+fgZpBNvbkjD8b+S0ECQA6Dg=="}, {Begin: "MIIETzCCAzegAwIBAgIUEbWzxg57E", End: "TT2Sljt0YfkmWfdXA\nwOUt"}, {Begin: "MIIEXjCCA0agAwIBAgIUEbWzxg57E", End: "ivyvRvlwCUNstG6u8Y7IxHHn"}, } // Do not count the empty first item if len(onlyCerts) != len(expected) { t.Errorf("Wrong number of certificates in the chain: got %d; want %d", len(onlyCerts), len(expected)) } for i, data := range expected { if !strings.HasPrefix(onlyCerts[i], data.Begin) || !strings.HasSuffix(onlyCerts[i], data.End+"\n-----END CERTIFICATE-----\n") { t.Errorf("Invalid certificate #%d, got:\n:%s", i, onlyCerts[i]) } } rootCert := string(rootCa) if !strings.HasPrefix(rootCert, "-----BEGIN CERTIFICATE-----\nMIIEVjCCAz6gAwIBAgIUA12e94NK") || !strings.HasSuffix(rootCert, "AQKotV5y5qBInw==\n-----END CERTIFICATE-----\n") { t.Errorf("Invalid root CA certificate, got:\n:%s", rootCert) } } func TestGetRsaKey(t *testing.T) { actual := string(GetRsaKey("testdata/RootCA.key", "secret")) if !strings.HasPrefix(actual, "-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAAS") || !strings.HasSuffix(actual, "DKY9SmW6QD+RJwbMc4M=\n-----END PRIVATE KEY-----\n") { t.Errorf("Unexpected generated RSA key: %s", actual) } } 07070100000073000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000002700000000uyuni-tools/mgradm/shared/ssl/testdata07070100000074000081B400000000000000000000000165DF46900000073E000000000000000000000000000000000000003200000000uyuni-tools/mgradm/shared/ssl/testdata/RootCA.key-----BEGIN ENCRYPTED PRIVATE KEY----- MIIFHTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIZDERQAIgS+ACAggA MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBA0CZ/wEFCBHg0mYxZucHcwBIIE wI40HqL0NoUUuxx0qo+UVoXuTPp5bWEmv4be+0v3+ya0ucCLx41l7HiYSwL/fYvt tStWm9ArOlLW48uxF54eyA3Nwvt59xnZBGN0YokJR1heeYzns1xadgHwiFjl4CLI IfpUoQvGmnph51301YRgwuTRGJsih8EH8g24TQ5qy1uRMS1BZLd7elUuHzVFyu6y PMfxpqcygBvjnufgOye4aAkTQIeGlJBDk5DwVx56T5VEUA8/v9VXVFy4took8Gwr B08ZVszbR7AeUpa16aqKXFV1hQPcEle/n/0zsqPTE0drRxAGKufdpvK1rfDRDSQx LudQJ31NJkCBRe+sP+rv4BA+zme4xFEjAI12IVQ6sUaTCW/qMnPBcSC5iSvVLXen iSB519BJgLPmJi2gNXsuluHddwRp1QFUOoxnyPHDyGFkQ4zNwnG/pPvxASoBQRPA PR/JsZC9p+oGHBds0HzZETuU5oKWdrZwyawQaAWEeuZEeICsrkrKi9PkumY5SMCT 47tn0vyQTQEGexKag/UTaC5PaGX8SxpWBXrskxgLcYnUAQnpkEhnU92CJQSI36uL pGC7kQKY8vPjCOw33lLrEkWDxIn3oFX76NuHtMBdKgNT/Qlxb1tqIfuaNI80MrGV zAzeK6dw4ZakI6aSlOBa4l71JB3HfWZjWAldhS33GvWAdsnPef5c5jMEsDqMFFnw lZh2hrdfUTkAb+v2tJ13CUccSqIy3i9DmOU/ijdEGGte3E1ws+qyouHsFC09ETcy XC4YYXF4ccGNZemcqpsQhi5iQEL+HjaNRkv16+qyXRUG49TGRe8nlEA2mBXV8Wzf mkEdzp9Oc3iOPqeQYgnhtHbYaj0iHjiwaUmKiisadD7Jo76z2CAG0YJdWw4FNNUI tpM2eADVHXcFGLDzvmFWtPnfkBnhn09GhOJJXOIEsoNyhczv6TqMrtGx8wYvbNqc YOgl42mDYn6v0P+uEDWcHWFQiHNDNPHUCT/LmjYcVRTOPlCqnv5Fh+yKS50pGtFy 8h53QURP7e4cJxE8CuBZAAGiEkoXEbXGoslnrtpQbB0bVpMDKrLpJMrMjSr4/pKK zWRgVo7wOuUJN+o30lcQ/RM8NzJRii6tnHRG1eOijNOLqRYmyEHFSeTB7+lhjE5A xyXotHOml32pW4lEu7Ks18fmTQtyI1opx4ocVLNMeRvesxCf3z5eMUciXG+frxnF a9UScTR1rIbVVP2V7PiHVCXY6WXZqiefWU/Sn+oVndm/9GGH9f2WVW1xn6YX3L+e pTpzZScReh7B0gd3cVtwmfAbQU3xuA8IFJnrLuDwQCs19WweImgPm/rf7ITBuxeO 3vdvVd8GfCsN3sTJ/9G5XJJstM0eXoXjoXhthQ4OuTyVSiI3tHROYW+iggAJsaVH HwzTnGE5m5WBvQ9GziSThxDz4vfDtwVlNS1K0+wkt9stt9ruCpXxntQV2FEin0GM iWptJuYpCMOH9gkALDYoYv/jG0PbMBs63FRLeZg2ehtYw6kU5KtjHkLZTiccuwSq W010SdsstOrwrZKEJAuRNQHLHRFmjyoskz0nCtmyJBCTkP46Awu5PFVwBE9auiIe Y0UUre0B4tPsoPewpvgRJio= -----END ENCRYPTED PRIVATE KEY----- 07070100000075000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/shared/ssl/testdata/chain107070100000076000081B400000000000000000000000165DF4690000027CA000000000000000000000000000000000000004200000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/intermediate-ca.crtCertificate: Data: Version: 3 (0x2) Serial Number: 67:63:f5:29:af:44:ba:79:ef:dd:dc:bf:bb:ab:3e:36:22:b8:71:26 Signature Algorithm: sha384WithRSAEncryption Issuer: C=DE, ST=STATE, L=CITY, O=ORG, OU=ORGUNIT, CN=RootCA Validity Not Before: Oct 2 13:09:11 2023 GMT Not After : Feb 13 13:09:11 2025 GMT Subject: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=OrgCa Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:e0:25:1f:af:4d:59:23:82:a6:f1:c9:33:d1:c6: 10:47:ad:f4:f2:b6:96:aa:79:2b:45:97:d3:d8:a5: 29:fe:9c:b9:2c:26:30:37:5f:ae:69:6a:ac:85:e0: 28:d3:d3:7e:83:c8:87:2a:70:e5:a6:78:99:d9:42: f9:d3:17:c0:e1:bc:4c:51:af:f0:aa:a8:fb:19:65: 27:91:35:80:5a:3d:fd:90:0b:fc:af:a1:d3:af:26: 02:ba:7c:26:bc:aa:08:6e:cf:d5:5d:ed:c9:8c:33: 9f:63:51:45:49:1f:f2:1f:12:c2:7c:e4:42:05:8a: ef:33:d3:0e:e5:44:58:99:88:aa:2f:e3:5f:37:d8: 36:fb:21:ac:90:0f:82:b9:55:bc:9e:ba:23:70:4a: 83:c0:44:37:c2:0a:9a:03:fb:1d:4a:d2:67:a8:70: e0:8c:b2:c1:d7:d8:e7:c9:bd:ee:6f:f6:4e:f7:25: f2:4b:9b:93:33:28:40:18:c6:f1:47:78:0d:84:fa: 7f:f4:82:9b:37:f0:37:84:25:b5:ae:f5:88:4f:d2: d9:7e:61:c0:8e:92:24:c8:32:55:cb:c4:8c:e6:be: 1f:e8:32:e2:9f:18:1e:2f:a6:8f:80:27:d4:77:7b: 5d:2d:cb:eb:a4:b8:2f:28:a0:38:34:a5:91:c8:6e: b6:71 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:TRUE X509v3 Key Usage: Digital Signature, Key Encipherment, Certificate Sign X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: 9B:C1:07:5E:AB:5C:7E:6B:9E:E4:23:7B:18:61:34:CB:D0:06:91:3B X509v3 Authority Key Identifier: keyid:6E:6D:4B:35:22:23:3E:13:18:A5:93:61:0E:9C:BE:1E:D2:B8:1B:D4 DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA serial:49:96:04:48:85:CB:0D:EC:2C:31:FE:EF:E9:CB:12:2B:DB:80:F8:71 Signature Algorithm: sha384WithRSAEncryption Signature Value: 78:5a:ac:de:87:a3:fb:d5:e1:29:4f:c3:1b:39:2b:da:29:78: 1c:07:3d:e3:db:da:8a:40:3d:c3:d4:51:9d:21:59:d2:37:66: f5:47:69:b8:96:2a:2e:f0:35:1a:5b:5b:23:cd:d5:ac:88:49: 97:e1:5b:91:e4:b8:7a:2d:ab:46:17:c4:61:a9:1a:b1:29:d3: 50:52:af:0d:c2:4a:e1:2f:aa:00:1b:07:5a:7d:f9:d1:57:19: 66:49:52:c5:74:3c:1e:3d:a3:1f:49:64:60:92:48:03:a2:37: 52:26:69:24:34:d7:a4:68:fd:ea:b7:a6:d6:c2:b0:46:19:a7: 2c:b9:cc:a3:0f:87:4c:cb:fb:69:d1:a9:c5:93:73:69:7c:34: aa:3d:f0:98:83:88:14:48:29:2d:ed:f9:c0:96:22:ae:03:7a: 2f:09:ad:43:6d:a3:12:d5:8c:48:e6:65:ca:e1:97:b4:ec:d7: aa:fc:db:e2:cf:16:30:2c:46:f3:dd:a5:37:db:d9:0f:99:c5: 74:e7:21:2d:ca:2b:c5:4b:50:56:0a:2c:0d:25:30:56:39:87: 33:b2:ae:d7:98:74:e9:3d:ce:78:ca:1b:bd:ea:f8:a6:3f:2a: a4:21:3b:19:9e:b5:0d:e6:6e:d8:d2:d6:b0:e2:aa:ce:54:e6: 51:98:65:ee -----BEGIN CERTIFICATE----- MIIEXjCCA0agAwIBAgIUZ2P1Ka9Eunnv3dy/u6s+NiK4cSYwDQYJKoZIhvcNAQEM BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD QTAeFw0yMzEwMDIxMzA5MTFaFw0yNTAyMTMxMzA5MTFaME0xCzAJBgNVBAYTAkRF MQ4wDAYDVQQIDAVTVEFURTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklU MQ4wDAYDVQQDDAVPcmdDYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AOAlH69NWSOCpvHJM9HGEEet9PK2lqp5K0WX09ilKf6cuSwmMDdfrmlqrIXgKNPT foPIhypw5aZ4mdlC+dMXwOG8TFGv8Kqo+xllJ5E1gFo9/ZAL/K+h068mArp8Jryq CG7P1V3tyYwzn2NRRUkf8h8SwnzkQgWK7zPTDuVEWJmIqi/jXzfYNvshrJAPgrlV vJ66I3BKg8BEN8IKmgP7HUrSZ6hw4IyywdfY58m97m/2Tvcl8kubkzMoQBjG8Ud4 DYT6f/SCmzfwN4Qlta71iE/S2X5hwI6SJMgyVcvEjOa+H+gy4p8YHi+mj4An1Hd7 XS3L66S4LyigODSlkchutnECAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYD VR0PBAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgB hvhCAQ0EGxYZU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUm8EH Xqtcfmue5CN7GGE0y9AGkTswgZoGA1UdIwSBkjCBj4AUbm1LNSIjPhMYpZNhDpy+ HtK4G9ShYaRfMF0xCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UE BwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQD DAZSb290Q0GCFEmWBEiFyw3sLDH+7+nLEivbgPhxMA0GCSqGSIb3DQEBDAUAA4IB AQB4Wqzeh6P71eEpT8MbOSvaKXgcBz3j29qKQD3D1FGdIVnSN2b1R2m4liou8DUa W1sjzdWsiEmX4VuR5Lh6LatGF8RhqRqxKdNQUq8NwkrhL6oAGwdaffnRVxlmSVLF dDwePaMfSWRgkkgDojdSJmkkNNekaP3qt6bWwrBGGacsucyjD4dMy/tp0anFk3Np fDSqPfCYg4gUSCkt7fnAliKuA3ovCa1DbaMS1YxI5mXK4Ze07Neq/NvizxYwLEbz 3aU329kPmcV05yEtyivFS1BWCiwNJTBWOYczsq7XmHTpPc54yhu96vimPyqkITsZ nrUN5m7Y0taw4qrOVOZRmGXu -----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) Serial Number: 67:63:f5:29:af:44:ba:79:ef:dd:dc:bf:bb:ab:3e:36:22:b8:71:27 Signature Algorithm: sha384WithRSAEncryption Issuer: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=OrgCa Validity Not Before: Oct 2 13:09:11 2023 GMT Not After : Nov 5 13:09:11 2024 GMT Subject: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=TeamCA Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:eb:35:fe:18:8f:59:de:23:4a:84:bc:de:7b:f1: 79:f8:1a:5d:94:95:54:2c:00:bd:42:c1:e6:f5:c6: ca:25:da:97:cd:5b:85:d6:89:8a:7c:45:11:9e:df: 65:10:68:e4:49:6a:cf:fd:76:48:08:c7:09:aa:e3: 88:c2:7e:2f:f9:85:b4:df:d4:00:ec:a9:71:38:1d: ff:8d:d4:d1:84:2a:f9:9b:e7:7f:e1:61:3e:75:06: 7f:18:66:59:23:96:e6:c2:75:11:e7:f4:f3:47:7b: e6:17:8c:25:d9:ee:da:01:d6:cd:94:9e:a7:8e:35: f6:d8:24:d6:cf:58:4f:29:36:42:18:96:aa:87:ca: ad:af:05:a2:e5:a6:6b:4f:42:98:e3:4e:86:b4:d7: 1f:2f:db:c3:5b:bd:e9:da:7d:d0:d9:8d:83:c9:28: 56:27:e7:0d:a2:15:88:99:af:eb:a3:85:73:9e:3d: 64:70:01:be:cb:71:c0:d8:ca:e7:6e:25:b7:3b:fe: 73:0a:92:d2:23:2d:f5:f4:9c:0e:d6:65:c6:ef:6c: 9a:c5:c5:af:70:10:ba:fc:2d:b1:29:26:88:9e:06: e5:63:5f:d4:25:0c:98:18:f0:46:77:86:f5:98:00: 63:38:3a:36:81:27:94:2a:cc:84:24:75:01:54:ed: a4:d7 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:TRUE X509v3 Key Usage: Digital Signature, Key Encipherment, Certificate Sign X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: 62:00:25:E4:EE:70:E5:37:2D:1E:9E:AE:4E:B7:3E:FC:62:08:BF:27 X509v3 Authority Key Identifier: keyid:9B:C1:07:5E:AB:5C:7E:6B:9E:E4:23:7B:18:61:34:CB:D0:06:91:3B DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA serial:67:63:F5:29:AF:44:BA:79:EF:DD:DC:BF:BB:AB:3E:36:22:B8:71:26 Signature Algorithm: sha384WithRSAEncryption Signature Value: 26:a0:7e:98:44:58:ab:81:9f:f9:a6:04:dc:08:59:d8:b4:5a: 11:47:8e:9c:23:7f:53:66:f9:b9:93:5b:df:50:d6:2a:11:a1: cb:1c:d5:2e:cd:5d:f3:eb:45:b5:fe:01:9f:0c:d0:f0:1d:8f: 57:ac:0f:2a:5b:a4:6a:57:a7:0e:25:e1:69:25:f5:ef:2f:3c: 60:9c:26:ac:e8:cd:3a:89:fa:84:18:da:bb:83:f6:f5:02:53: 51:f2:ab:76:e8:fb:d0:63:dc:5c:09:c5:f7:de:68:90:c0:50: 80:ec:88:ff:16:95:a0:c1:97:69:fb:1f:9d:43:32:0c:5d:f9: bc:5e:48:c4:52:f2:f3:43:1f:ff:c5:bb:58:6c:ee:11:cb:0c: 22:45:29:1c:62:26:78:9c:31:10:d8:14:24:17:17:4a:7d:a9: 1d:3b:5b:64:8e:b2:84:91:66:fb:f2:e6:37:3d:c2:1b:db:98: 11:10:6d:67:9c:95:a5:d9:a4:8a:e1:b6:c4:ab:2d:f7:48:3a: 66:9b:9c:af:9b:b7:a5:27:cc:4b:53:e6:21:0c:2b:6c:c8:b2: cc:6c:51:58:df:b2:bc:53:ed:25:0f:4a:e6:44:6c:be:74:46: 0b:6a:df:46:76:cd:c3:8d:c0:9b:93:3c:93:50:22:a1:f4:fa: 38:9a:85:1f -----BEGIN CERTIFICATE----- MIIETzCCAzegAwIBAgIUZ2P1Ka9Eunnv3dy/u6s+NiK4cScwDQYJKoZIhvcNAQEM BQAwTTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx EDAOBgNVBAsMB09SR1VOSVQxDjAMBgNVBAMMBU9yZ0NhMB4XDTIzMTAwMjEzMDkx MVoXDTI0MTEwNTEzMDkxMVowTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRF MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1D QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOs1/hiPWd4jSoS83nvx efgaXZSVVCwAvULB5vXGyiXal81bhdaJinxFEZ7fZRBo5Elqz/12SAjHCarjiMJ+ L/mFtN/UAOypcTgd/43U0YQq+Zvnf+FhPnUGfxhmWSOW5sJ1Eef080d75heMJdnu 2gHWzZSep4419tgk1s9YTyk2QhiWqofKra8FouWma09CmONOhrTXHy/bw1u96dp9 0NmNg8koVifnDaIViJmv66OFc549ZHABvstxwNjK524ltzv+cwqS0iMt9fScDtZl xu9smsXFr3AQuvwtsSkmiJ4G5WNf1CUMmBjwRneG9ZgAYzg6NoEnlCrMhCR1AVTt pNcCAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgKkMB0GA1Ud JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgBhvhCAQ0EGxYZU1NMIEdl bmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUYgAl5O5w5TctHp6uTrc+/GII vycwgZoGA1UdIwSBkjCBj4AUm8EHXqtcfmue5CN7GGE0y9AGkTuhYaRfMF0xCzAJ BgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UE CgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0GCFGdj9Smv RLp5793cv7urPjYiuHEmMA0GCSqGSIb3DQEBDAUAA4IBAQAmoH6YRFirgZ/5pgTc CFnYtFoRR46cI39TZvm5k1vfUNYqEaHLHNUuzV3z60W1/gGfDNDwHY9XrA8qW6Rq V6cOJeFpJfXvLzxgnCas6M06ifqEGNq7g/b1AlNR8qt26PvQY9xcCcX33miQwFCA 7Ij/FpWgwZdp+x+dQzIMXfm8XkjEUvLzQx//xbtYbO4RywwiRSkcYiZ4nDEQ2BQk FxdKfakdO1tkjrKEkWb78uY3PcIb25gREG1nnJWl2aSK4bbEqy33SDpmm5yvm7el J8xLU+YhDCtsyLLMbFFY37K8U+0lD0rmRGy+dEYLat9Gds3DjcCbkzyTUCKh9Po4 moUf -----END CERTIFICATE----- 07070100000077000081B400000000000000000000000165DF4690000013BA000000000000000000000000000000000000003A00000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/root-ca.crtCertificate: Data: Version: 3 (0x2) Serial Number: 49:96:04:48:85:cb:0d:ec:2c:31:fe:ef:e9:cb:12:2b:db:80:f8:71 Signature Algorithm: sha256WithRSAEncryption Issuer: C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA Validity Not Before: Oct 2 13:09:10 2023 GMT Not After : Jul 22 13:09:10 2026 GMT Subject: C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:f3:67:90:0c:b8:98:e1:5c:8d:00:32:13:26:95: 95:6f:7c:2f:96:34:b8:6e:2f:5a:61:da:e2:bd:2e: 9e:8a:ad:5e:4e:27:a9:1c:08:06:7c:36:26:28:5e: a7:6e:bc:04:68:eb:2c:97:b6:4b:ca:f0:0d:9c:5a: 47:ee:9e:15:1e:c0:62:3c:72:1b:80:01:07:67:51: 64:34:0f:41:50:73:21:09:d9:79:ac:73:51:db:5c: a0:30:fa:79:49:02:a4:8e:cb:8f:15:dd:99:4c:b7: 9e:d1:ec:18:f9:6f:d2:73:27:d1:ff:c9:07:07:4e: 8a:6e:02:2d:6d:ab:5b:5f:5b:a2:4a:4d:c7:d6:7b: 26:6e:b7:a0:44:0d:82:18:43:f8:3a:49:f7:47:40: d0:ed:72:dd:f2:8b:c4:9f:e5:64:24:49:f0:0d:e8: 5b:21:66:89:31:4a:3e:1e:9c:9b:11:89:91:9d:57: af:73:64:19:bf:ed:02:8f:3f:0b:5f:aa:2c:5c:93: 9b:03:08:c4:a1:72:58:7a:df:cb:f2:00:8c:71:7e: 76:23:29:ac:c6:6a:46:2a:a9:b2:6e:f4:14:2a:16: e8:7b:3c:f4:c3:14:89:11:54:d3:10:70:6c:98:c8: 66:e3:f7:31:cf:fd:78:76:e2:eb:2a:3e:37:a6:ce: 4c:07 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:TRUE X509v3 Key Usage: Digital Signature, Key Encipherment, Certificate Sign X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: 6E:6D:4B:35:22:23:3E:13:18:A5:93:61:0E:9C:BE:1E:D2:B8:1B:D4 X509v3 Authority Key Identifier: DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA serial:49:96:04:48:85:CB:0D:EC:2C:31:FE:EF:E9:CB:12:2B:DB:80:F8:71 Signature Algorithm: sha256WithRSAEncryption Signature Value: 82:e6:ae:cb:60:cf:85:3e:00:70:37:c7:dc:c9:51:b9:70:36: 25:e2:f5:bc:e0:8f:34:3d:67:1f:09:8e:48:e9:de:b5:78:b5: b5:97:f6:75:fa:fc:0f:05:c4:e1:33:ab:f5:f9:b1:32:9f:75: b3:c4:fd:a9:6d:c6:88:c6:a5:35:68:28:04:1d:c0:1d:92:9a: 9b:be:52:e9:b9:9c:0d:01:b1:a8:0d:42:89:f7:f3:43:58:99: 98:6c:0d:9f:ff:9d:10:29:68:9f:db:41:e7:b7:c6:43:67:79: ec:a6:f2:5b:ce:b7:d9:17:90:c2:f4:ac:56:8f:9a:af:fb:85: 85:59:95:d1:5e:37:f6:40:2a:16:cf:53:fa:55:8b:35:48:31: 10:c6:c4:b9:85:07:96:48:c3:dd:35:d0:04:e3:c8:fd:7e:8e: a2:ab:60:6b:b4:cc:f5:33:44:f8:bc:e6:b1:1b:86:f0:6d:a1: 23:48:62:63:de:e3:27:d7:8c:9b:58:a2:10:ed:11:b6:b8:4c: ee:83:4a:be:0b:ee:6a:80:1d:02:91:21:4d:84:64:5f:a5:1e: 1b:c5:8c:c6:d9:7d:0c:43:da:45:c9:13:e5:47:46:8d:bb:36: 51:f8:72:70:d7:40:43:97:3b:71:fc:55:90:00:6e:8f:d3:c9: c6:73:67:15 -----BEGIN CERTIFICATE----- MIIEVjCCAz6gAwIBAgIUSZYESIXLDewsMf7v6csSK9uA+HEwDQYJKoZIhvcNAQEL BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD QTAeFw0yMzEwMDIxMzA5MTBaFw0yNjA3MjIxMzA5MTBaMF0xCzAJBgNVBAYTAkRF MQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAw DgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0EwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDzZ5AMuJjhXI0AMhMmlZVvfC+WNLhuL1ph2uK9Lp6K rV5OJ6kcCAZ8NiYoXqduvARo6yyXtkvK8A2cWkfunhUewGI8chuAAQdnUWQ0D0FQ cyEJ2Xmsc1HbXKAw+nlJAqSOy48V3ZlMt57R7Bj5b9JzJ9H/yQcHTopuAi1tq1tf W6JKTcfWeyZut6BEDYIYQ/g6SfdHQNDtct3yi8Sf5WQkSfAN6FshZokxSj4enJsR iZGdV69zZBm/7QKPPwtfqixck5sDCMShclh638vyAIxxfnYjKazGakYqqbJu9BQq Fuh7PPTDFIkRVNMQcGyYyGbj9zHP/Xh24usqPjemzkwHAgMBAAGjggEMMIIBCDAM BgNVHRMEBTADAQH/MAsGA1UdDwQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI KwYBBQUHAwIwKAYJYIZIAYb4QgENBBsWGVNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh dGUwHQYDVR0OBBYEFG5tSzUiIz4TGKWTYQ6cvh7SuBvUMIGCBgNVHSMEezB5oWGk XzBdMQswCQYDVQQGEwJERTEOMAwGA1UECAwFU1RBVEUxDTALBgNVBAcMBENJVFkx DDAKBgNVBAoMA09SRzEQMA4GA1UECwwHT1JHVU5JVDEPMA0GA1UEAwwGUm9vdENB ghRJlgRIhcsN7Cwx/u/pyxIr24D4cTANBgkqhkiG9w0BAQsFAAOCAQEAguauy2DP hT4AcDfH3MlRuXA2JeL1vOCPND1nHwmOSOnetXi1tZf2dfr8DwXE4TOr9fmxMp91 s8T9qW3GiMalNWgoBB3AHZKam75S6bmcDQGxqA1CiffzQ1iZmGwNn/+dEClon9tB 57fGQ2d57KbyW8632ReQwvSsVo+ar/uFhVmV0V439kAqFs9T+lWLNUgxEMbEuYUH lkjD3TXQBOPI/X6Ooqtga7TM9TNE+LzmsRuG8G2hI0hiY97jJ9eMm1iiEO0RtrhM 7oNKvgvuaoAdApEhTYRkX6UeG8WMxtl9DEPaRckT5UdGjbs2UfhycNdAQ5c7cfxV kABuj9PJxnNnFQ== -----END CERTIFICATE----- 07070100000078000081B400000000000000000000000165DF46900000148A000000000000000000000000000000000000003900000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/server.crtCertificate: Data: Version: 3 (0x2) Serial Number: 67:63:f5:29:af:44:ba:79:ef:dd:dc:bf:bb:ab:3e:36:22:b8:71:28 Signature Algorithm: sha384WithRSAEncryption Issuer: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=TeamCA Validity Not Before: Oct 2 13:09:12 2023 GMT Not After : Oct 1 13:09:12 2024 GMT Subject: C=DE, ST=STATE, O=ORG, OU=ORGUNIT, CN=uyuni-server-cert Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:83:51:78:0e:31:92:ea:d9:51:6e:9d:02:ed:d8: 55:dc:53:5f:7e:0c:a5:26:df:e1:c1:86:e0:38:9d: 67:59:e2:42:21:37:4d:5d:2c:f7:28:f1:d4:7f:00: 91:e4:0e:fa:eb:c8:bb:2d:f2:cd:37:62:5e:94:67: 83:0a:e0:69:4d:86:f4:39:be:1b:59:ec:64:65:41: 0c:5a:3a:7e:4b:98:8e:62:4c:4b:2c:b7:68:3c:36: e2:ea:7e:58:70:e7:3e:7e:0a:b4:7f:32:b7:d0:0f: 15:b4:ac:24:ce:a8:f4:13:9e:62:44:7d:f6:3e:fd: a5:67:5a:3d:67:54:40:89:6f:51:f0:4a:60:35:d4: 51:27:ca:bb:a1:5e:32:12:a5:3f:b4:e3:d0:7d:9e: b2:84:4f:4e:84:db:52:00:9e:46:bf:c8:31:3b:79: 30:09:fe:ba:01:b7:3e:c4:9b:c4:cc:18:8b:f0:42: fd:88:71:8c:d6:4c:d2:99:c8:7a:bf:7e:d6:dc:18: d2:97:57:ca:0c:0c:66:52:2e:38:1c:8a:56:13:44: a5:84:3d:95:70:e5:aa:94:2f:59:48:3d:22:74:8d: f8:5d:ef:42:9c:e1:cf:ca:f5:24:d8:a8:8c:4e:1d: 75:9d:ac:15:a9:6f:f7:81:ab:5e:11:61:f7:8f:e3: a5:c1 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:FALSE X509v3 Key Usage: Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Cert Type: SSL Server Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: FD:F8:AA:26:08:7F:F7:FA:92:1E:68:3F:4A:29:54:E5:7A:A6:0D:42 X509v3 Authority Key Identifier: keyid:62:00:25:E4:EE:70:E5:37:2D:1E:9E:AE:4E:B7:3E:FC:62:08:BF:27 DirName:/C=DE/ST=STATE/O=ORG/OU=ORGUNIT/CN=OrgCa serial:67:63:F5:29:AF:44:BA:79:EF:DD:DC:BF:BB:AB:3E:36:22:B8:71:27 X509v3 Subject Alternative Name: DNS:*.example.com Signature Algorithm: sha384WithRSAEncryption Signature Value: 24:4e:b4:4d:29:e6:ad:12:e6:39:9d:95:0e:fc:b7:af:e3:55: 60:cc:f2:57:1c:38:05:fb:1f:8c:95:40:40:b3:23:c3:11:1b: 5f:7b:99:01:0d:fe:3a:05:7e:d0:b1:9a:c8:fc:6a:78:41:fb: 3f:5a:6a:26:0d:dd:2e:f3:ab:d5:16:45:99:51:3e:94:87:3a: a7:67:e4:25:43:c9:1a:e5:84:df:15:ba:f3:11:64:99:1d:22: 0e:44:35:9c:9b:52:e8:b0:a8:a9:04:d2:4b:cf:10:14:35:6c: 1f:a3:ec:81:4f:2b:98:c0:02:8a:b9:03:50:f3:97:25:25:05: ba:c4:e0:5b:b2:34:7e:d3:d6:e1:69:d8:72:38:9e:0a:50:f6: 25:d0:97:c3:58:37:49:40:08:11:9d:39:b1:4b:4d:e9:18:08: 38:9d:b5:b4:a2:8b:d8:24:94:b3:b5:41:e9:17:b8:22:17:6e: 70:33:a2:a3:ee:8e:ae:be:ee:c4:dd:6c:2c:c2:ae:8b:31:8f: 5d:ca:9d:01:83:5d:89:59:cf:f6:30:3a:59:4d:17:82:ab:6e: a3:bf:4d:61:98:9a:f6:29:20:9b:eb:c5:3a:cd:06:b6:82:8c: 24:34:65:60:12:6d:4b:ca:e6:91:0a:58:c8:97:22:89:f5:f4: 3f:16:33:4e -----BEGIN CERTIFICATE----- MIIEdDCCA1ygAwIBAgIUZ2P1Ka9Eunnv3dy/u6s+NiK4cSgwDQYJKoZIhvcNAQEM BQAwTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx EDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1DQTAeFw0yMzEwMDIxMzA5 MTJaFw0yNDEwMDExMzA5MTJaMFkxCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFU RTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMRowGAYDVQQDDBF1eXVu aS1zZXJ2ZXItY2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAINR eA4xkurZUW6dAu3YVdxTX34MpSbf4cGG4DidZ1niQiE3TV0s9yjx1H8AkeQO+uvI uy3yzTdiXpRngwrgaU2G9Dm+G1nsZGVBDFo6fkuYjmJMSyy3aDw24up+WHDnPn4K tH8yt9APFbSsJM6o9BOeYkR99j79pWdaPWdUQIlvUfBKYDXUUSfKu6FeMhKlP7Tj 0H2esoRPToTbUgCeRr/IMTt5MAn+ugG3PsSbxMwYi/BC/YhxjNZM0pnIer9+1twY 0pdXygwMZlIuOByKVhNEpYQ9lXDlqpQvWUg9InSN+F3vQpzhz8r1JNiojE4ddZ2s Falv94GrXhFh94/jpcECAwEAAaOCAT0wggE5MAkGA1UdEwQCMAAwCwYDVR0PBAQD AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjARBglghkgBhvhCAQEE BAMCBkAwKAYJYIZIAYb4QgENBBsWGVNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUw HQYDVR0OBBYEFP34qiYIf/f6kh5oP0opVOV6pg1CMIGJBgNVHSMEgYEwf4AUYgAl 5O5w5TctHp6uTrc+/GIIvyehUaRPME0xCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVT VEFURTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ4wDAYDVQQDDAVP cmdDYYIUZ2P1Ka9Eunnv3dy/u6s+NiK4cScwGAYDVR0RBBEwD4INKi5leGFtcGxl LmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAJE60TSnmrRLmOZ2VDvy3r+NVYMzyVxw4 BfsfjJVAQLMjwxEbX3uZAQ3+OgV+0LGayPxqeEH7P1pqJg3dLvOr1RZFmVE+lIc6 p2fkJUPJGuWE3xW68xFkmR0iDkQ1nJtS6LCoqQTSS88QFDVsH6PsgU8rmMACirkD UPOXJSUFusTgW7I0ftPW4WnYcjieClD2JdCXw1g3SUAIEZ05sUtN6RgIOJ21tKKL 2CSUs7VB6Re4IhducDOio+6Orr7uxN1sLMKuizGPXcqdAYNdiVnP9jA6WU0Xgqtu o79NYZia9ikgm+vFOs0GtoKMJDRlYBJtS8rmkQpYyJciifX0PxYzTg== -----END CERTIFICATE----- 07070100000079000081B400000000000000000000000165DF4690000006A8000000000000000000000000000000000000003900000000uyuni-tools/mgradm/shared/ssl/testdata/chain1/server.key-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCDUXgOMZLq2VFu nQLt2FXcU19+DKUm3+HBhuA4nWdZ4kIhN01dLPco8dR/AJHkDvrryLst8s03Yl6U Z4MK4GlNhvQ5vhtZ7GRlQQxaOn5LmI5iTEsst2g8NuLqflhw5z5+CrR/MrfQDxW0 rCTOqPQTnmJEffY+/aVnWj1nVECJb1HwSmA11FEnyruhXjISpT+049B9nrKET06E 21IAnka/yDE7eTAJ/roBtz7Em8TMGIvwQv2IcYzWTNKZyHq/ftbcGNKXV8oMDGZS LjgcilYTRKWEPZVw5aqUL1lIPSJ0jfhd70Kc4c/K9STYqIxOHXWdrBWpb/eBq14R YfeP46XBAgMBAAECggEAHjz9Sy9pKEEAelsXWJNvOfvMyma5BNmaz4hySzcbnFv4 ZFOqseDvzPLavp/v+Dbm2rJvP6ZgUPeK1dt8Fl4UgXCo/j7jZ3KCr7op0QEVIe0w JDxzNwnIq8zrtZmAXgcxoa5vX7bbEsLWebMGCrxm77mR4TmsIVcg5kqmRwvkjIDM SZxAJSMYVWlmyI695fMPng8f4nOxRWPBgW73XzMvlyr68OUtji9+JqI8C9mJ471S F2qL+ubaovZM83EQ1gAol5RX8rDdkQD+/OlkLaVvdKOpp3VLKx1KszYyqhR/wYqD 4FUjK5Abxz8JlVOZkPOaF3Y9IgAuAhK45lqyUjGtvQKBgQC3wTzcpB2zjmw07iAP B0/REU1RGPvkehsV3KnjsSvWAzrgAJMgS1l5pWJLLBRJDguX3jbDMiQ05Z9RSLdL +IbvL75+QhbXLRiv7aGkM1xIaBrGJpCANZmHKjjy/T7LfewSwyNqlP/FdMOy+tat 2/t/IrmpArryd/cP/Pw8rfKS0wKBgQC28oixIJP3uAtQzpiqKnIovw57s8MXcMzh QaxR00v8ss4d82T6BmJolF+gjotsYq4lVnOmL1+E/74dN8p0xL8ITIrr8AAO2Kp2 j0qWVgagvjqTq+5H7er9dRwCpKTo2dt2/JM6MCrKyNICdZdljYJiUVijTenckzeS K78RD3BAmwKBgQCu0PNjAeuT4IJHVOhBA/bGcsx4w+kYs6ZDBTzHds26fDYt174g 8i58kX/S/muKGQekgu7cgz546J/KSADCEP3mXii/m4Z5Tdj3vn6SZZ588DXQn+3H W7blJaEqYw2zsOe/7dAq3Pf8VZq9EvDcVLWOfW3eQc+zT7hHiKo73E0zqwKBgHa+ 0a52gNRnJyEaF8lLp7F+4T21nkmWs8T5xYmO5mFtBZA3LTGD91f+BlvGagS9wF8H 0CTr1soS3SlFzykfkwcl933Q15jLVUmDFFykFcU78/VpwU36xW4iFz4387oXvfVr V3yLSxs4Yeeqv8vwn9KFDk1hAwximc1Mi8XdCXVFAoGAVIYfKAO9KgshtbxRE3ql kC7DhT2iZ7du8F2qLZf5tEV4WcWtxy5vI89MYHzg1MhToKPGauvOxDSqdLLuzeKa 0MWuGiV02z4nA5xv40OVWI5zylcwPeV7drCoitjvbFCpv4bKcagdVOOF8SjB25GA yBPHa/QCsfYNCPpWHS7DYvk= -----END PRIVATE KEY----- 0707010000007A000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002E00000000uyuni-tools/mgradm/shared/ssl/testdata/chain20707010000007B000081B400000000000000000000000165DF46900000061E000000000000000000000000000000000000004700000000uyuni-tools/mgradm/shared/ssl/testdata/chain2/RHN-ORG-TRUSTED-SSL-CERT-----BEGIN CERTIFICATE----- MIIEVjCCAz6gAwIBAgIUA12e94NKtyrGIZpdEYgrqkjHXN8wDQYJKoZIhvcNAQEL BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD QTAeFw0yMzEwMDYxNTQyMzBaFw0yNjA3MjYxNTQyMzBaMF0xCzAJBgNVBAYTAkRF MQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAw DgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0EwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDDbR3+UOxtw6KO8s/XsvkjukqSAFggAjSJxOw+KJL4 tOykM4lBXkC3nLiV6ve5Np2koi9bX1At/nk1Fxftwy37WbeVAFs6wprkI0sDbK6z ZfT/qRoNChpYnzMFs28VCgftsOv1q5aLEUHfnSgEIK3lH3lMvDaEO6VgTDa84Y3h DlNbj5bssq3mMHsKE5DRCSM0wXP8ZlnwfY8S/LMxf8FN8S+c3fwg6/+dUKiAHU8Q goXQliH/NvZZPvvYTiADTY+xt6fEeZ4OdVVV31V7so3v6cIN4WwaOtGAzWKOrB4r Oa4ZybhmEMW7rLOnSUvl+r1UyWfh/8rH+ATSYQSynI/TAgMBAAGjggEMMIIBCDAM BgNVHRMEBTADAQH/MAsGA1UdDwQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI KwYBBQUHAwIwKAYJYIZIAYb4QgENBBsWGVNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNh dGUwHQYDVR0OBBYEFDOCM0j6AGn+E4QnDh2Y9rR5Mc1DMIGCBgNVHSMEezB5oWGk XzBdMQswCQYDVQQGEwJERTEOMAwGA1UECAwFU1RBVEUxDTALBgNVBAcMBENJVFkx DDAKBgNVBAoMA09SRzEQMA4GA1UECwwHT1JHVU5JVDEPMA0GA1UEAwwGUm9vdENB ghQDXZ73g0q3KsYhml0RiCuqSMdc3zANBgkqhkiG9w0BAQsFAAOCAQEAbvvh+GyX KwFC8xaGAAHBsz0yg43LS5W9TNNOospC1qwbgCpSZJ9nWBbF2UWTKUgzjSPUpCjJ 0kMUvkpFnwFujE8IgJiP0Tha3KE3D14kj91Vfs5jDSyBsexUi8GMTP4caTMbXnU5 +q1iVhigbtOh2gSBKQvTIIdhzhghp9iFX7f68WERRVlSG/xGCSt6DXO5sgUyQb2U ArHMJZkROIrVeGY6pXp1dWB/j6iguRUTC3GJ0JfRgx5E+pgpFnjItDQ1e2/pxsQr ikqyFuc2CAHkhlEl0oWz+yWwCQrKkZNLABWyPWtnMecIoCqZQ79EoeQ59JZzQPrg AQKotV5y5qBInw== -----END CERTIFICATE----- 0707010000007C000081B400000000000000000000000165DF469000003C9D000000000000000000000000000000000000003C00000000uyuni-tools/mgradm/shared/ssl/testdata/chain2/spacewalk.crtCertificate: Data: Version: 3 (0x2) Serial Number: 11:b5:b3:c6:0e:7b:13:28:c9:e6:28:3c:f1:40:25:6d:cb:14:eb:3e Signature Algorithm: sha384WithRSAEncryption Issuer: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = TeamCA Validity Not Before: Oct 6 15:42:31 2023 GMT Not After : Oct 5 15:42:31 2024 GMT Subject: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = uyuni.world-co.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:b8:3f:47:da:13:fe:f4:7b:af:ff:75:fc:0b:dd: e7:26:d6:34:7b:19:33:80:7d:b9:20:40:17:b8:34: a4:80:3a:4c:bb:25:0c:8a:40:65:47:32:04:af:ef: 2d:b6:97:70:66:1e:23:28:b0:8f:98:d4:f0:2c:b6: c0:40:6a:29:06:c2:8d:5e:81:5b:60:66:54:54:9f: fd:77:ae:b4:62:63:87:f1:5b:fb:aa:41:cc:82:16: 10:3e:35:9d:99:98:63:c1:ad:2c:7b:2d:02:0e:0a: af:1d:75:6d:5c:44:c1:3d:a8:28:a5:a4:53:35:10: 5b:58:a8:ab:54:77:ad:f4:f4:e7:5a:51:5f:75:6f: 05:37:fd:55:56:a2:4d:2e:3a:58:3a:a4:d6:ad:20: 6d:4f:7e:1d:a2:83:94:a2:6c:0c:b8:03:ba:39:55: 05:93:ad:7c:9f:7a:12:99:28:3e:53:9d:3a:83:bc: 4a:3e:6e:2e:52:e6:63:a2:fa:e7:d9:12:90:2c:5b: 78:52:34:92:19:19:ac:28:84:c3:25:4f:8f:f9:0d: 64:ef:eb:e4:bc:cd:87:89:1c:74:01:6f:e2:1a:78: 92:e2:2e:15:d0:8e:2b:94:69:6d:87:f4:91:f1:5d: f3:47:73:95:e3:d6:80:87:93:15:6a:f7:ae:af:83: b6:55 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:FALSE X509v3 Key Usage: Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Cert Type: SSL Server Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: 93:88:1D:52:E3:27:6E:73:35:9E:0E:AE:20:9F:E2:2E:58:41:CA:90 X509v3 Authority Key Identifier: keyid:C6:C6:FD:7F:A3:EA:C1:50:0A:7E:33:1D:50:F7:E0:94:3F:93:EA:B7 DirName:/C=DE/ST=STATE/O=ORG/OU=ORGUNIT/CN=OrgCa serial:11:B5:B3:C6:0E:7B:13:28:C9:E6:28:3C:F1:40:25:6D:CB:14:EB:3D X509v3 Subject Alternative Name: DNS:uyuni.world-co.com Signature Algorithm: sha384WithRSAEncryption 75:04:ec:e4:e7:cc:3b:00:df:a3:5e:42:70:f4:30:91:17:8c: 2a:19:58:6d:0c:0c:ff:8a:5e:b8:1f:03:e5:c5:01:7e:7f:c1: 9d:25:3d:5a:89:df:0b:97:a4:f6:94:3b:ce:fc:11:f2:db:b2: 4f:76:5a:4e:9a:6d:ef:b9:b5:69:db:c7:33:27:d8:8b:ce:a7: 45:e5:12:84:38:48:b3:f3:54:6f:bf:fb:35:3f:ae:26:1a:03: b2:e1:55:45:97:eb:d2:b3:7e:d3:bd:f3:21:d0:34:56:51:15: 88:e6:49:e3:ea:ac:e8:aa:5c:16:d2:95:fa:f2:d6:f6:f0:5a: e7:8b:c1:7e:f6:54:c5:a4:36:99:0c:ef:d9:c3:9d:d4:22:f9: 55:d1:2b:10:ed:6c:9d:84:87:88:c2:b3:bf:ac:54:fa:3e:3d: 42:5d:76:83:cb:9a:b9:a2:88:b3:99:31:ac:05:f2:d6:16:be: 73:85:bd:56:49:17:6a:f6:81:e4:f7:ec:2a:38:50:11:b6:c6: af:6e:df:8a:97:57:f6:36:b6:ca:3c:04:e0:c6:2b:20:c2:c0: 50:f7:21:ec:46:23:e5:3c:5d:e3:37:19:48:88:3c:40:10:fb: bd:86:40:52:bf:9f:81:9a:41:36:f6:e4:8c:3f:1b:f9:2d:04: 09:00:3a:0e -----BEGIN CERTIFICATE----- MIIEejCCA2KgAwIBAgIUEbWzxg57EyjJ5ig88UAlbcsU6z4wDQYJKoZIhvcNAQEM BQAwTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx EDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1DQTAeFw0yMzEwMDYxNTQy MzFaFw0yNDEwMDUxNTQyMzFaMFoxCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFU RTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMRswGQYDVQQDDBJ1eXVu aS53b3JsZC1jby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4 P0faE/70e6//dfwL3ecm1jR7GTOAfbkgQBe4NKSAOky7JQyKQGVHMgSv7y22l3Bm HiMosI+Y1PAstsBAaikGwo1egVtgZlRUn/13rrRiY4fxW/uqQcyCFhA+NZ2ZmGPB rSx7LQIOCq8ddW1cRME9qCilpFM1EFtYqKtUd6309OdaUV91bwU3/VVWok0uOlg6 pNatIG1Pfh2ig5SibAy4A7o5VQWTrXyfehKZKD5TnTqDvEo+bi5S5mOi+ufZEpAs W3hSNJIZGawohMMlT4/5DWTv6+S8zYeJHHQBb+IaeJLiLhXQjiuUaW2H9JHxXfNH c5Xj1oCHkxVq966vg7ZVAgMBAAGjggFCMIIBPjAJBgNVHRMEAjAAMAsGA1UdDwQE AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEQYJYIZIAYb4QgEB BAQDAgZAMCgGCWCGSAGG+EIBDQQbFhlTU0wgR2VuZXJhdGVkIENlcnRpZmljYXRl MB0GA1UdDgQWBBSTiB1S4yduczWeDq4gn+IuWEHKkDCBiQYDVR0jBIGBMH+AFMbG /X+j6sFQCn4zHVD34JQ/k+q3oVGkTzBNMQswCQYDVQQGEwJERTEOMAwGA1UECAwF U1RBVEUxDDAKBgNVBAoMA09SRzEQMA4GA1UECwwHT1JHVU5JVDEOMAwGA1UEAwwF T3JnQ2GCFBG1s8YOexMoyeYoPPFAJW3LFOs9MB0GA1UdEQQWMBSCEnV5dW5pLndv cmxkLWNvLmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAdQTs5OfMOwDfo15CcPQwkReM KhlYbQwM/4peuB8D5cUBfn/BnSU9WonfC5ek9pQ7zvwR8tuyT3ZaTppt77m1advH MyfYi86nReUShDhIs/NUb7/7NT+uJhoDsuFVRZfr0rN+073zIdA0VlEViOZJ4+qs 6KpcFtKV+vLW9vBa54vBfvZUxaQ2mQzv2cOd1CL5VdErEO1snYSHiMKzv6xU+j49 Ql12g8uauaKIs5kxrAXy1ha+c4W9VkkXavaB5PfsKjhQEbbGr27fipdX9ja2yjwE 4MYrIMLAUPch7EYj5Txd4zcZSIg8QBD7vYZAUr+fgZpBNvbkjD8b+S0ECQA6Dg== -----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) Serial Number: 11:b5:b3:c6:0e:7b:13:28:c9:e6:28:3c:f1:40:25:6d:cb:14:eb:3d Signature Algorithm: sha384WithRSAEncryption Issuer: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = OrgCa Validity Not Before: Oct 6 15:42:30 2023 GMT Not After : Nov 9 15:42:30 2024 GMT Subject: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = TeamCA Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:87:88:e3:ca:8e:8a:f1:5e:1e:b4:78:1d:32:79: ef:bd:51:74:fb:40:8d:85:01:98:a4:b3:73:fa:18: f5:5f:7c:6c:fb:56:ad:30:ee:df:da:19:cb:db:d2: f8:59:8b:15:52:6a:46:c2:1c:12:4d:ed:83:a5:67: 97:47:9e:98:94:78:e2:fd:e4:7e:18:48:12:92:29: 54:49:ac:bb:8e:de:db:c2:22:37:a9:4f:0d:ff:39: 5a:ca:98:2b:fd:b5:ec:e2:e1:88:2a:cf:b6:3a:60: 1b:11:74:a2:af:fa:e6:a7:b4:71:21:f7:d9:6c:2f: c5:33:d4:e2:fd:b1:93:8d:de:ff:2c:86:52:e9:84: 19:dd:ba:a6:0b:85:f4:64:ef:15:97:79:21:a9:da: 46:ef:b5:89:00:01:e0:6d:72:21:6b:ea:a3:7c:d1: 42:8a:26:ca:7c:f2:47:a8:8e:86:2b:a9:1b:61:66: 02:93:ab:57:cf:e4:7b:94:08:7f:71:62:f1:29:23: 35:a4:33:6c:e1:84:4c:c4:91:aa:45:b3:d4:a7:6b: 83:80:6b:ec:03:27:73:ff:10:20:1c:fd:aa:3d:79: f7:4f:cf:c4:83:bd:4c:b1:5e:55:1c:f4:49:34:23: c3:01:fc:25:b0:45:81:da:cc:10:84:66:e1:9b:c2: 4a:57 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:TRUE X509v3 Key Usage: Digital Signature, Key Encipherment, Certificate Sign X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: C6:C6:FD:7F:A3:EA:C1:50:0A:7E:33:1D:50:F7:E0:94:3F:93:EA:B7 X509v3 Authority Key Identifier: keyid:5B:5B:28:4B:13:37:60:B5:95:D4:5B:47:09:97:59:DF:16:63:AF:D9 DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA serial:11:B5:B3:C6:0E:7B:13:28:C9:E6:28:3C:F1:40:25:6D:CB:14:EB:3C Signature Algorithm: sha384WithRSAEncryption 92:16:c7:1b:6c:7a:f9:a1:dc:57:bd:24:45:26:0a:72:91:75: 38:bc:f0:2c:d3:9f:ab:7a:bf:11:c0:1a:60:f3:5d:6a:ba:fe: f7:83:c4:21:f9:72:02:eb:47:85:16:6b:c9:38:58:6b:06:5f: c8:55:c1:ac:6e:9d:3c:ca:20:d1:94:15:d8:86:ed:b6:58:fc: 56:81:15:8d:53:8f:62:da:5a:15:74:b0:78:41:da:fc:c1:69: fb:8d:cc:65:86:de:e5:79:f8:2d:53:1c:5c:c8:76:50:07:fe: f5:31:46:73:ba:e8:be:bb:f3:63:09:ae:f4:91:22:99:68:f6: 82:b3:52:e5:92:4c:91:c8:12:b2:df:48:71:cc:ee:44:22:db: 7e:52:97:5f:99:13:96:06:31:67:8c:00:c0:31:62:57:9a:aa: 82:fe:e5:d9:56:07:fa:2a:15:fa:47:01:e4:ce:b0:98:fe:c0: ab:67:6d:7b:dc:30:d5:51:f5:13:7e:44:48:2a:d5:6f:4d:ab: 22:0c:1a:45:bb:d3:36:37:aa:5c:f6:d3:6e:6e:d7:83:8c:3d: 60:f5:b4:c9:67:f0:f3:a0:3a:6a:9e:7d:8c:e0:8e:75:9c:9c: 9b:25:9b:cc:71:a2:53:4f:64:a5:8e:dd:18:7e:49:96:7d:d5: c0:c0:e5:2d -----BEGIN CERTIFICATE----- MIIETzCCAzegAwIBAgIUEbWzxg57EyjJ5ig88UAlbcsU6z0wDQYJKoZIhvcNAQEM BQAwTTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQwwCgYDVQQKDANPUkcx EDAOBgNVBAsMB09SR1VOSVQxDjAMBgNVBAMMBU9yZ0NhMB4XDTIzMTAwNjE1NDIz MFoXDTI0MTEwOTE1NDIzMFowTjELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRF MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlRlYW1D QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIeI48qOivFeHrR4HTJ5 771RdPtAjYUBmKSzc/oY9V98bPtWrTDu39oZy9vS+FmLFVJqRsIcEk3tg6Vnl0ee mJR44v3kfhhIEpIpVEmsu47e28IiN6lPDf85WsqYK/217OLhiCrPtjpgGxF0oq/6 5qe0cSH32WwvxTPU4v2xk43e/yyGUumEGd26pguF9GTvFZd5IanaRu+1iQAB4G1y IWvqo3zRQoomynzyR6iOhiupG2FmApOrV8/ke5QIf3Fi8SkjNaQzbOGETMSRqkWz 1Kdrg4Br7AMnc/8QIBz9qj1590/PxIO9TLFeVRz0STQjwwH8JbBFgdrMEIRm4ZvC SlcCAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgKkMB0GA1Ud JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgBhvhCAQ0EGxYZU1NMIEdl bmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUxsb9f6PqwVAKfjMdUPfglD+T 6rcwgZoGA1UdIwSBkjCBj4AUW1soSxM3YLWV1FtHCZdZ3xZjr9mhYaRfMF0xCzAJ BgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UEBwwEQ0lUWTEMMAoGA1UE CgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQDDAZSb290Q0GCFBG1s8YO exMoyeYoPPFAJW3LFOs8MA0GCSqGSIb3DQEBDAUAA4IBAQCSFscbbHr5odxXvSRF JgpykXU4vPAs05+rer8RwBpg811quv73g8Qh+XIC60eFFmvJOFhrBl/IVcGsbp08 yiDRlBXYhu22WPxWgRWNU49i2loVdLB4Qdr8wWn7jcxlht7lefgtUxxcyHZQB/71 MUZzuui+u/NjCa70kSKZaPaCs1LlkkyRyBKy30hxzO5EItt+UpdfmROWBjFnjADA MWJXmqqC/uXZVgf6KhX6RwHkzrCY/sCrZ2173DDVUfUTfkRIKtVvTasiDBpFu9M2 N6pc9tNubteDjD1g9bTJZ/DzoDpqnn2M4I51nJybJZvMcaJTT2Sljt0YfkmWfdXA wOUt -----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) Serial Number: 11:b5:b3:c6:0e:7b:13:28:c9:e6:28:3c:f1:40:25:6d:cb:14:eb:3c Signature Algorithm: sha384WithRSAEncryption Issuer: C = DE, ST = STATE, L = CITY, O = ORG, OU = ORGUNIT, CN = RootCA Validity Not Before: Oct 6 15:42:30 2023 GMT Not After : Feb 17 15:42:30 2025 GMT Subject: C = DE, ST = STATE, O = ORG, OU = ORGUNIT, CN = OrgCa Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:c5:6a:66:7e:44:c7:79:2c:85:ab:72:48:b8:d3: c2:a4:33:a9:ec:4d:4a:4c:9a:cb:f7:d8:a9:38:81: 70:12:f2:6c:b5:31:f4:f9:2b:5c:d3:e6:d1:d3:7e: 97:a7:ab:30:06:6a:82:13:15:3b:bf:1c:b5:9a:81: c6:da:ab:8b:10:b7:3e:ec:21:63:72:fd:6e:cd:6e: 83:53:af:aa:d1:1e:66:79:42:03:50:aa:71:a5:6e: ac:8f:5d:1a:b1:21:35:65:10:56:7f:fb:59:f7:f7: 3c:1c:41:1d:a3:bd:98:a5:df:a6:00:9a:9f:a9:f4: f4:10:3f:1d:63:9e:dc:ab:44:3d:8a:2a:bc:70:7f: 56:e0:bd:ca:1b:45:54:94:72:db:12:02:22:c9:07: f5:cf:60:6f:a8:b5:b0:cc:8e:16:25:33:21:f3:3a: ac:7a:12:2c:f4:f6:25:55:be:98:4a:d0:cc:5a:25: 82:16:27:70:6b:d3:4d:f6:10:0f:2d:75:03:1f:90: a9:31:28:24:78:c3:4a:af:54:69:46:a4:5c:c0:3a: 6b:94:5f:3e:b8:86:b9:40:ce:c1:0f:bf:de:5b:cf: 59:14:49:49:cb:d7:27:d5:d0:d2:14:b9:5b:4d:0a: 90:1c:e3:2b:8c:c4:d5:8d:a7:9c:db:2b:60:36:45: e7:3d Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: CA:TRUE X509v3 Key Usage: Digital Signature, Key Encipherment, Certificate Sign X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication Netscape Comment: SSL Generated Certificate X509v3 Subject Key Identifier: 5B:5B:28:4B:13:37:60:B5:95:D4:5B:47:09:97:59:DF:16:63:AF:D9 X509v3 Authority Key Identifier: keyid:33:82:33:48:FA:00:69:FE:13:84:27:0E:1D:98:F6:B4:79:31:CD:43 DirName:/C=DE/ST=STATE/L=CITY/O=ORG/OU=ORGUNIT/CN=RootCA serial:03:5D:9E:F7:83:4A:B7:2A:C6:21:9A:5D:11:88:2B:AA:48:C7:5C:DF Signature Algorithm: sha384WithRSAEncryption 9e:36:a2:c3:9f:7c:88:74:be:19:70:3f:bd:bf:44:93:86:1e: e7:72:96:66:62:d6:45:ee:c3:22:d8:09:13:2c:96:26:e8:dc: 72:8d:6d:c4:51:68:32:58:ec:4b:45:1b:59:58:fb:ef:bc:91: c2:f3:ed:c6:4f:70:8b:e1:48:f7:7e:b4:b6:91:98:1d:a1:0e: e0:08:36:ff:d7:8f:79:d8:5d:76:f6:49:d7:c1:9e:24:58:dd: 48:77:69:8e:80:82:ec:f5:5a:44:0d:b8:7d:5c:8e:ce:b0:1d: e7:3c:b4:73:10:e6:1b:9e:fb:45:42:34:64:98:58:a2:da:4b: b9:3f:df:61:c2:1e:25:f8:8e:84:3d:2c:e7:a9:43:54:2d:39: 3b:9c:f9:9b:22:e1:37:dd:46:25:11:a1:c6:3a:60:18:56:56: 8d:e0:99:31:8a:5b:ad:a5:4f:4b:b5:d4:cf:ca:91:93:1b:d4: 41:16:56:85:fd:99:df:0d:48:c1:0c:af:4a:60:e0:d2:9e:9b: 18:81:58:fe:54:f2:42:bc:60:70:d4:f8:0c:70:4a:b3:3f:90: 0b:63:f3:1b:b1:2e:40:c2:ef:59:ab:49:9b:26:22:c2:09:8e: ec:39:d0:95:8a:fc:af:46:f9:70:09:43:6c:b4:6e:ae:f1:8e: c8:c4:71:e7 -----BEGIN CERTIFICATE----- MIIEXjCCA0agAwIBAgIUEbWzxg57EyjJ5ig88UAlbcsU6zwwDQYJKoZIhvcNAQEM BQAwXTELMAkGA1UEBhMCREUxDjAMBgNVBAgMBVNUQVRFMQ0wCwYDVQQHDARDSVRZ MQwwCgYDVQQKDANPUkcxEDAOBgNVBAsMB09SR1VOSVQxDzANBgNVBAMMBlJvb3RD QTAeFw0yMzEwMDYxNTQyMzBaFw0yNTAyMTcxNTQyMzBaME0xCzAJBgNVBAYTAkRF MQ4wDAYDVQQIDAVTVEFURTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklU MQ4wDAYDVQQDDAVPcmdDYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AMVqZn5Ex3kshatySLjTwqQzqexNSkyay/fYqTiBcBLybLUx9PkrXNPm0dN+l6er MAZqghMVO78ctZqBxtqrixC3PuwhY3L9bs1ug1OvqtEeZnlCA1CqcaVurI9dGrEh NWUQVn/7Wff3PBxBHaO9mKXfpgCan6n09BA/HWOe3KtEPYoqvHB/VuC9yhtFVJRy 2xICIskH9c9gb6i1sMyOFiUzIfM6rHoSLPT2JVW+mErQzFolghYncGvTTfYQDy11 Ax+QqTEoJHjDSq9UaUakXMA6a5RfPriGuUDOwQ+/3lvPWRRJScvXJ9XQ0hS5W00K kBzjK4zE1Y2nnNsrYDZF5z0CAwEAAaOCASQwggEgMAwGA1UdEwQFMAMBAf8wCwYD VR0PBAQDAgKkMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAoBglghkgB hvhCAQ0EGxYZU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUW1so SxM3YLWV1FtHCZdZ3xZjr9kwgZoGA1UdIwSBkjCBj4AUM4IzSPoAaf4ThCcOHZj2 tHkxzUOhYaRfMF0xCzAJBgNVBAYTAkRFMQ4wDAYDVQQIDAVTVEFURTENMAsGA1UE BwwEQ0lUWTEMMAoGA1UECgwDT1JHMRAwDgYDVQQLDAdPUkdVTklUMQ8wDQYDVQQD DAZSb290Q0GCFANdnveDSrcqxiGaXRGIK6pIx1zfMA0GCSqGSIb3DQEBDAUAA4IB AQCeNqLDn3yIdL4ZcD+9v0SThh7ncpZmYtZF7sMi2AkTLJYm6NxyjW3EUWgyWOxL RRtZWPvvvJHC8+3GT3CL4Uj3frS2kZgdoQ7gCDb/14952F129knXwZ4kWN1Id2mO gILs9VpEDbh9XI7OsB3nPLRzEOYbnvtFQjRkmFii2ku5P99hwh4l+I6EPSznqUNU LTk7nPmbIuE33UYlEaHGOmAYVlaN4JkxilutpU9LtdTPypGTG9RBFlaF/ZnfDUjB DK9KYODSnpsYgVj+VPJCvGBw1PgMcEqzP5ALY/MbsS5Awu9Zq0mbJiLCCY7sOdCV ivyvRvlwCUNstG6u8Y7IxHHn -----END CERTIFICATE----- 0707010000007D000081B400000000000000000000000165DF4690000006A8000000000000000000000000000000000000003C00000000uyuni-tools/mgradm/shared/ssl/testdata/chain2/spacewalk.key-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4P0faE/70e6// dfwL3ecm1jR7GTOAfbkgQBe4NKSAOky7JQyKQGVHMgSv7y22l3BmHiMosI+Y1PAs tsBAaikGwo1egVtgZlRUn/13rrRiY4fxW/uqQcyCFhA+NZ2ZmGPBrSx7LQIOCq8d dW1cRME9qCilpFM1EFtYqKtUd6309OdaUV91bwU3/VVWok0uOlg6pNatIG1Pfh2i g5SibAy4A7o5VQWTrXyfehKZKD5TnTqDvEo+bi5S5mOi+ufZEpAsW3hSNJIZGawo hMMlT4/5DWTv6+S8zYeJHHQBb+IaeJLiLhXQjiuUaW2H9JHxXfNHc5Xj1oCHkxVq 966vg7ZVAgMBAAECggEAErkNgGX5Sdda2GshKISNdYcdcKfsMaG1Awe4UVX6JHSo MPlQF6l5ET3ON6Gmw9AKUjo8SOl+QiHbYTPWAAW5sw/opUKgakCjz7CtZXDZsEjc euSlw5Spp0t+LZAtunq/omIKa970P0CLMINrEE4FVBJnRQPYl8MYgT8sn+oEgaiI J0p1MaxvvLWJfCF9niFWevCxjWwFNP5nYA7XdfdG8yKO7AXIlE3Xte174juTQ5+Q neAEYnh2bp+uqEgfgvhp830NsBmegvqn7USSUqWXO++aTEwlDv70t5YWtD6GvSu2 8SWEPdAsLZkH1tzD+0jgMhdiJdcsY48fSnOxP1LHSQKBgQDI+SBLwtvAtEHI1lY7 LQ+lonJSNY/MtFEkPBAgDtU97PzF7ucoQdYitoLOyAu2d6TN7ze/H8crKjLSbKXn X5DcID+7fLWbAeF4jDUfB9cm/zMkywxHcWL3K+aHzD08NVdTcGpIxViUZ/7ekBOr 9l+32+tRkYLumH9roUsSnLN0DQKBgQDqscbR8OtzwGi2ic/cjJvyZ1Vkbr19yeea 2WM0RiSO3vU7NY7aBSWkoHOAECKdKqJ0J7VhhMYYP+LKGFQbQQge+LmQpDRIJaM9 tq5QpOW6YYpwvGLwsbHSwfz93iRR+SO9+X9mx/FeoyOv9qKBvxv3UzdNpO5W5BKt 3oJiqbVRaQKBgQCC8sB2XNru7wTGJdI98Jh3ZidzJW8zBHKyV2hyWvfax6XUGlwH wQ4TxDPrJDFtjPuXKz15jO0rVO2UajKXVY9/vouIUDPMcidFcqXSODuaL0JVwO+Z RWokfzhQV2W261KhDWhTTjLvT+ujfOE0dO3dULA9j8BuUnMD4C6YS/4pqQKBgQC3 2oSyOlV43CYruVIIqG4SOzj98HKpc93nxJyeesRw1+CsfYxm5tlSWg+hJwK2tIuH CwRgXK8CmCmFwAFDSHKgMKDN2pTKYBG9arqrmkIM/BSDtFCd1dZEEIusJLW3McD6 NdXEIqXHSW3PjxpHIfs6iQot3SKJFyo64rCpseDE4QKBgHHp6c9UEJjq+IYT6hXg kvxS87ZWWVqFRAjS93WRS7ZllFyX9+9GfUAEaOrArJf8gvVl+8QOqVn9spjUoV0o VIWMPlh9VRS9nQGYNTTg3vYRRCdNE0SGwNg4CL7oW1kvoPOVMk7nST/9AnGAMpkF LY7A0vS56vx3wZuWfPbqcK1s -----END PRIVATE KEY----- 0707010000007E000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002400000000uyuni-tools/mgradm/shared/templates0707010000007F000081B400000000000000000000000165DF4690000002D5000000000000000000000000000000000000003700000000uyuni-tools/mgradm/shared/templates/inspectTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" "github.com/uyuni-project/uyuni-tools/shared/types" ) const inspectTemplate = `#!/bin/bash # inspect.sh, generated by mgradm {{- range .Param }} echo "{{ .Variable }}=$({{ .CLI }})" >> {{ $.OutputFile }} {{- end }} exit 0 ` // InspectTemplateData represents information used to create inspect script. type InspectTemplateData struct { Param []types.InspectData OutputFile string } // Render will create inspect script. func (data InspectTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("inspect").Parse(inspectTemplate)) return t.Execute(wr, data) } 07070100000080000081B400000000000000000000000165DF469000000811000000000000000000000000000000000000003600000000uyuni-tools/mgradm/shared/templates/issuerTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) // Deploy self-signed issuer or CA Certificate and key. const issuerTemplate = `{{if and .Certificate .Key -}} apiVersion: v1 kind: Secret type: kubernetes.io/tls metadata: name: uyuni-ca namespace: {{ .Namespace }} data: ca.crt: {{ .RootCa }} tls.crt: {{ .Certificate }} tls.key: {{ .Key }} {{- else }} apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: uyuni-issuer namespace: {{ .Namespace }} spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: uyuni-ca namespace: {{ .Namespace }} spec: isCA: true {{- if or .Country .State .City .Org .OrgUnit }} subject: {{- if .Country }} countries: ["{{ .Country }}"] {{- end }} {{- if .State }} provinces: ["{{ .State }}"] {{- end }} {{- if .City }} localities: ["{{ .City }}"] {{- end }} {{- if .Org }} organizations: ["{{ .Org }}"] {{- end }} {{- if .OrgUnit }} organizationalUnits: ["{{ .OrgUnit }}"] {{- end }} {{- end }} {{- if .Email }} emailAddresses: - {{ .Email }} {{- end }} commonName: {{ .Fqdn }} dnsNames: - {{ .Fqdn }} secretName: uyuni-ca privateKey: algorithm: ECDSA size: 256 issuerRef: name: uyuni-issuer kind: Issuer group: cert-manager.io {{- end }} --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: uyuni-ca-issuer namespace: {{ .Namespace }} spec: ca: secretName: uyuni-ca ` // IssuerTemplateData represents information used to create issuer file. type IssuerTemplateData struct { Namespace string Country string State string City string Org string OrgUnit string Email string Fqdn string RootCa string Certificate string Key string } // Render creates issuer file. func (data IssuerTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("issuer").Parse(issuerTemplate)) return t.Execute(wr, data) } 07070100000081000081B400000000000000000000000165DF46900000049D000000000000000000000000000000000000003E00000000uyuni-tools/mgradm/shared/templates/mgrSetupScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const mgrSetupScriptTemplate = `#!/bin/sh {{- range $name, $value := .Env }} export {{ $name }}={{ $value }} {{- end }} {{- if .DebugJava }} echo 'JAVA_OPTS=" $JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=*:8003,server=y,suspend=n" ' >> /etc/tomcat/conf.d/remote_debug.conf echo 'JAVA_OPTS=" $JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=*:8001,server=y,suspend=n" ' >> /etc/rhn/taskomatic.conf echo 'JAVA_OPTS=" $JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=*:8002,server=y,suspend=n" ' >> /usr/share/rhn/config-defaults/rhn_search_daemon.conf {{- end }} /usr/lib/susemanager/bin/mgr-setup -s -n # clean before leaving rm $0` // MgrSetupScriptTemplateData represents information used to create setup script. type MgrSetupScriptTemplateData struct { Env map[string]string DebugJava bool } // Render will create setup script. func (data MgrSetupScriptTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("script").Parse(mgrSetupScriptTemplate)) return t.Execute(wr, data) } 07070100000082000081B400000000000000000000000165DF469000001598000000000000000000000000000000000000003D00000000uyuni-tools/mgradm/shared/templates/migrateScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" "github.com/uyuni-project/uyuni-tools/shared/types" ) const migrationScriptTemplate = `#!/bin/bash set -e SSH_CONFIG="" if test -e /tmp/ssh_config; then SSH_CONFIG="-F /tmp/ssh_config" fi SSH="ssh -A $SSH_CONFIG " SCP="scp -A $SSH_CONFIG " echo "Stopping spacewalk service..." $SSH {{ .SourceFqdn }} "spacewalk-service stop ; systemctl start postgresql.service" $SSH {{ .SourceFqdn }} \ "echo \"COPY (SELECT MIN(CONCAT(org_id, '-', label)) AS target, base_path FROM rhnKickstartableTree GROUP BY base_path) TO STDOUT WITH CSV;\" \ |spacewalk-sql --select-mode - " > distros echo "Stopping posgresql service..." $SSH {{ .SourceFqdn }} "systemctl stop postgresql.service" while IFS="," read -r target path ; do echo "-/ $path" done < distros > exclude_list # exclude all config files which already exist and are not marked noreplace rpm -qa --qf '[%{fileflags},%{filenames}\n]' |grep ",/etc/" | while IFS="," read -r flags path ; do # config(noreplace) is 1<<4 (from lib/rpmlib.h) if [ $(( $flags & 16 )) -eq 0 -a -f "$path" ] ; then echo "-/ $path" >> exclude_list fi done # exclude schema migration files echo "-/ /etc/sysconfig/rhn/reportdb-schema-upgrade" >> exclude_list echo "-/ /etc/sysconfig/rhn/schema-upgrade" >> exclude_list for folder in {{ range .Volumes }}{{ . }} {{ end }}; do if $SSH {{ .SourceFqdn }} test -e $folder; then echo "Copying $folder..." rsync -e "$SSH" --rsync-path='sudo rsync' -avz -f "merge exclude_list" {{ .SourceFqdn }}:$folder/ $folder; else echo "Skipping missing $folder..." fi done; sed -i -e 's|appBase="webapps"|appBase="/usr/share/susemanager/www/tomcat/webapps"|' /etc/tomcat/server.xml sed -i -e 's|DocumentRoot\s*"/srv/www/htdocs"|DocumentRoot "/usr/share/susemanager/www/htdocs"|' /etc/apache2/vhosts.d/vhost-ssl.conf echo "Migrating auto-installable distributions..." while IFS="," read -r target path ; do if $SSH -A {{ .SourceFqdn }} test -e $path; then echo "Copying distribution $target from $path" mkdir -p "/srv/www/distributions/$target" rsync -e "$SSH" --rsync-path='sudo rsync' -avz "{{ .SourceFqdn }}:$path/" "/srv/www/distributions/$target" else echo "Skipping missing distribution $path..." fi done < distros rm -f /srv/www/htdocs/pub/RHN-ORG-TRUSTED-SSL-CERT; ln -s /etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT /srv/www/htdocs/pub/RHN-ORG-TRUSTED-SSL-CERT; echo "Extracting time zone..." $SSH {{ .SourceFqdn }} timedatectl show -p Timezone >/var/lib/uyuni-tools/data echo "Extracting postgresql versions..." echo "new_pg_version=$(rpm -qa --qf '%{VERSION}\n' 'name=postgresql[0-8][0-9]-server' | cut -d. -f1 | sort -n | tail -1)" >> /var/lib/uyuni-tools/data echo "old_pg_version=$(cat /var/lib/pgsql/data/PG_VERSION)" >> /var/lib/uyuni-tools/data echo "Altering configuration for domain resolution..." sed 's/report_db_host = {{ .SourceFqdn }}/report_db_host = localhost/' -i /etc/rhn/rhn.conf; sed 's/server\.jabber_server/java\.hostname/' -i /etc/rhn/rhn.conf; sed 's/client_use_localhost: false/client_use_localhost: true/' -i /etc/cobbler/settings.yaml; echo "Altering configuration for container environment..." sed 's/address=[^:]*:/address=*:/' -i /etc/rhn/taskomatic.conf; if test ! -f /etc/tomcat/conf.d/remote_debug.conf -a -f /etc/sysconfig/tomcat; then mv /etc/sysconfig/tomcat /etc/tomcat/conf.d/remote_debug.conf fi sed 's/address=[^:]*:/address=*:/' -i /etc/tomcat/conf.d/remote_debug.conf {{ if .Kubernetes }} echo 'server.no_ssl = 1' >> /etc/rhn/rhn.conf; echo "Extracting SSL certificate and authority" extractedSSL= if test -d /root/ssl-build; then # We may have an old unused ssl-build folder, check if the CA matches the deployed one buildCaFingerprint= if test -e /root/ssl-build/RHN-ORG-TRUSTED-SSL-CERT; then buildCaFingerprint=$(openssl x509 -in /root/ssl-build/RHN-ORG-TRUSTED-SSL-CERT -noout -fingerprint) fi caFingerprint=$(openssl x509 -in /etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT -noout -fingerprint) if test "$buildCaFingerprint" == "$caFingerprint"; then echo "Extracting SSL Root CA key..." # Extract the SSL CA certificate and key. # The server certificate will be auto-generated by cert-manager using it, so no need to copy it. cp /root/ssl-build/RHN-ORG-PRIVATE-SSL-KEY /var/lib/uyuni-tools/ extractedSSL="1" fi fi # This Root CA file is common to both cases cp /etc/pki/trust/anchors/LOCAL-RHN-ORG-TRUSTED-SSL-CERT /var/lib/uyuni-tools/RHN-ORG-TRUSTED-SSL-CERT if test "extractedSSL" != "1"; then # For third party certificates, the CA chain is in the certificate file. $SCP {{ .SourceFqdn }}:/etc/pki/tls/private/spacewalk.key /var/lib/uyuni-tools/ $SCP {{ .SourceFqdn }}:/etc/pki/tls/certs/spacewalk.crt /var/lib/uyuni-tools/ fi echo "Removing useless ssl-build folder..." rm -rf /root/ssl-build # The content of this folder will be a RO mount from a configmap rm /etc/pki/trust/anchors/* {{ end }} echo "DONE"` // MigrateScriptTemplateData represents migration information used to create migration script. type MigrateScriptTemplateData struct { Volumes []types.VolumeMount SourceFqdn string Kubernetes bool } // Render will create migration script. func (data MigrateScriptTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("script").Parse(migrationScriptTemplate)) return t.Execute(wr, data) } 07070100000083000081B400000000000000000000000165DF469000000748000000000000000000000000000000000000004300000000uyuni-tools/mgradm/shared/templates/pgsqlFinalizeScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const postgresFinalizeScriptTemplate = `#!/bin/bash set -e {{ if .RunAutotune }} echo "Running smdba system-check autotuning..." smdba system-check autotuning {{ end }} echo "Starting Postgresql..." su -s /bin/bash - postgres -c "/usr/share/postgresql/postgresql-script start" {{ if .RunReindex }} echo "Reindexing database. This may take a while, please do not cancel it!" database=$(sed -n "s/^\s*db_name\s*=\s*\([^ ]*\)\s*$/\1/p" /etc/rhn/rhn.conf) spacewalk-sql --select-mode - <<<"REINDEX DATABASE \"${database}\";" {{ end }} {{ if .RunSchemaUpdate }} echo "Schema update..." /usr/sbin/spacewalk-startup-helper check-database {{ end }} {{ if .RunDistroMigration }} echo "Updating auto-installable distributions..." spacewalk-sql --select-mode - <<EOT SELECT MIN(CONCAT(org_id, '-', label)) AS target, base_path INTO TEMP TABLE dist_map FROM rhnKickstartableTree GROUP BY base_path; UPDATE rhnKickstartableTree SET base_path = CONCAT('/srv/www/distributions/', target) from dist_map WHERE dist_map.base_path = rhnKickstartableTree.base_path; DROP TABLE dist_map; EOT {{ end }} echo "Stopping Postgresql..." su -s /bin/bash - postgres -c "/usr/share/postgresql/postgresql-script stop" echo "DONE" ` // FinalizePostgresTemplateData represents information used to create PostgreSQL migration script. type FinalizePostgresTemplateData struct { RunAutotune bool RunReindex bool RunSchemaUpdate bool RunDistroMigration bool Kubernetes bool } // Render will create script for finalizing PostgreSQL upgrade. func (data FinalizePostgresTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("script").Parse(postgresFinalizeScriptTemplate)) return t.Execute(wr, data) } 07070100000084000081B400000000000000000000000165DF4690000008D3000000000000000000000000000000000000004900000000uyuni-tools/mgradm/shared/templates/pgsqlVersionUpgradeScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const postgreSQLVersionUpgradeScriptTemplate = `#!/bin/bash set -e echo "PostgreSQL version upgrade" OLD_VERSION={{ .OldVersion }} NEW_VERSION={{ .NewVersion }} FAST_UPGRADE=--link echo "Testing presence of postgresql$NEW_VERSION..." test -d /usr/lib/postgresql$NEW_VERSION/bin echo "Testing presence of postgresql$OLD_VERSION..." test -d /usr/lib/postgresql$OLD_VERSION/bin echo "Create a backup at /var/lib/pgsql/data-pg$OLD_VERSION..." mv /var/lib/pgsql/data /var/lib/pgsql/data-pg$OLD_VERSION echo "Create new database directory..." mkdir -p /var/lib/pgsql/data chown -R postgres:postgres /var/lib/pgsql echo "Enforce key permission" chown -R postgres:postgres /etc/pki/tls/private/pg-spacewalk.key chown -R postgres:postgres /etc/pki/tls/certs/spacewalk.crt echo "Initialize new postgresql $NEW_VERSION database..." . /etc/sysconfig/postgresql 2>/dev/null # Load locale for SUSE PGHOME=$(getent passwd postgres | awk -F: '{print $6}') #. $PGHOME/.i18n 2>/dev/null # Load locale for Enterprise Linux if [ -z $POSTGRES_LANG ]; then POSTGRES_LANG="en_US.UTF-8" [ ! -z $LC_CTYPE ] && POSTGRES_LANG=$LC_CTYPE fi echo "Running initdb using postgres user" echo "Any suggested command from the console should be run using postgres user" su -s /bin/bash - postgres -c "initdb -D /var/lib/pgsql/data --locale=$POSTGRES_LANG" echo "Successfully initialized new postgresql $NEW_VERSION database." su -s /bin/bash - postgres -c "pg_upgrade --old-bindir=/usr/lib/postgresql$OLD_VERSION/bin --new-bindir=/usr/lib/postgresql$NEW_VERSION/bin --old-datadir=/var/lib/pgsql/data-pg$OLD_VERSION --new-datadir=/var/lib/pgsql/data $FAST_UPGRADE" echo "DONE"` // PostgreSQLVersionUpgradeTemplateData represents information used to create PostgreSQL migration script. type PostgreSQLVersionUpgradeTemplateData struct { OldVersion string NewVersion string Kubernetes bool } // Render will create PostgreSQL migration script. func (data PostgreSQLVersionUpgradeTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("script").Parse(postgreSQLVersionUpgradeScriptTemplate)) return t.Execute(wr, data) } 07070100000085000081B400000000000000000000000165DF4690000003A8000000000000000000000000000000000000004100000000uyuni-tools/mgradm/shared/templates/postUpgradeScriptTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const postUpgradeScriptTemplate = `#!/bin/bash set -e {{ if .CobblerHost }} sed 's/cobbler\.host.*/cobbler\.host = {{ .CobblerHost }}/' -i /etc/rhn/rhn.conf; echo 'redhat_management_server_use_localhost = true' > /etc/cobbler/settings.d/redhat_management_server_use_localhost.conf echo 'redhat_management_server_ignore_certificate = true' > /etc/cobbler/settings.d/redhat_management_server_use_localhost.conf {{ end }} ` // PostUpgradeTemplateData represents information used to create post upgrade. type PostUpgradeTemplateData struct { CobblerHost string } // Render will create script for finalizing PostgreSQL upgrade. func (data PostUpgradeTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("script").Parse(postUpgradeScriptTemplate)) return t.Execute(wr, data) } 07070100000086000081B400000000000000000000000165DF469000000810000000000000000000000000000000000000003700000000uyuni-tools/mgradm/shared/templates/serviceTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" "github.com/uyuni-project/uyuni-tools/shared/types" ) const serviceTemplate = `# uyuni-server.service, generated by mgradm # Use an uyuni-server.service.d/local.conf file to override [Unit] Description=Uyuni server image container service Wants=network.target After=network-online.target RequiresMountsFor=%t/containers [Service] Environment=PODMAN_SYSTEMD_UNIT=%n Environment=TZ={{ .Timezone }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-server.pid %t/%n.ctr-id ExecStartPre=/usr/bin/podman rm --ignore --force -t 10 {{ .NamePrefix }}-server ExecStart=/usr/bin/podman run \ --conmon-pidfile %t/uyuni-server.pid \ --cidfile=%t/%n.ctr-id \ --cgroups=no-conmon \ --sdnotify=conmon \ -d \ --name {{ .NamePrefix }}-server \ --hostname {{ .NamePrefix }}-server \ {{ .Args }} \ {{- range .Ports }} -p {{ .Exposed }}:{{ .Port }}{{if .Protocol}}/{{ .Protocol }}{{end}} \ {{- end }} {{- range .Volumes }} -v {{ .Name }}:{{ .MountPath }} \ {{- end }} -e TZ=${TZ} \ --network {{ .Network }} \ ${UYUNI_IMAGE} ExecStop=/usr/bin/podman exec \ uyuni-server \ /bin/bash -c 'spacewalk-service stop && systemctl stop postgresql' ExecStop=/usr/bin/podman stop \ --ignore -t 10 \ --cidfile=%t/%n.ctr-id ExecStopPost=/usr/bin/podman rm \ -f \ --ignore -t 10 \ --cidfile=%t/%n.ctr-id PIDFile=%t/uyuni-server.pid TimeoutStopSec=180 TimeoutStartSec=900 Type=forking [Install] WantedBy=multi-user.target default.target ` // PodmanServiceTemplateData POD information to create systemd file. type PodmanServiceTemplateData struct { Volumes []types.VolumeMount NamePrefix string Args string Ports []types.PortMap Timezone string Image string Network string } // Render will create the systemd configuration file. func (data PodmanServiceTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(serviceTemplate)) return t.Execute(wr, data) } 07070100000087000081B400000000000000000000000165DF46900000034F000000000000000000000000000000000000003100000000uyuni-tools/mgradm/shared/templates/tlsSecret.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) // Deploy self-signed issuer or CA Certificate and key. const tlsSecretTemplate = `apiVersion: v1 kind: Secret type: kubernetes.io/tls metadata: name: {{ .Name }} namespace: {{ .Namespace }} data: ca.crt: {{ .RootCa }} tls.crt: {{ .Certificate }} tls.key: {{ .Key }} ` // TlsSecretTemplateData contains information to create secret configuration file. type TlsSecretTemplateData struct { Name string Namespace string RootCa string Certificate string Key string } // Render creates secret configuration file. func (data TlsSecretTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("secret").Parse(tlsSecretTemplate)) return t.Execute(wr, data) } 07070100000088000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002000000000uyuni-tools/mgradm/shared/utils07070100000089000081B400000000000000000000000165DF469000000B07000000000000000000000000000000000000002D00000000uyuni-tools/mgradm/shared/utils/cmd_utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "fmt" "path" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgradm/shared/ssl" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) var defaultImage = path.Join(utils.DefaultNamespace, "server") // HelmFrags stores Uyuni and Cert Manager Helm information. type HelmFlags struct { Uyuni types.ChartFlags CertManager types.ChartFlags } // SslCertFlags can store SSL Certs information. type SslCertFlags struct { Cnames []string `mapstructure:"cname"` Country string State string City string Org string OU string Password string Email string Ca ssl.CaChain Server ssl.SslPair } // UseExisting return true if existing SSL Cert can be used. func (f *SslCertFlags) UseExisting() bool { return f.Server.Cert != "" && f.Server.Key != "" && f.Ca.Root != "" } // Checks that all the required flags are passed if using 3rd party certificates. func (f *SslCertFlags) CheckParameters() { if !f.UseExisting() && (f.Server.Cert != "" || f.Server.Key != "" || f.Ca.Root != "") { log.Fatal().Msg("Server certificate, key and root CA need to be all provided") } } // AddHelmInstallFlag add Helm install flags to a command. func AddHelmInstallFlag(cmd *cobra.Command) { defaultChart := fmt.Sprintf("oci://%s/server-helm", utils.DefaultNamespace) cmd.Flags().String("helm-uyuni-namespace", "default", "Kubernetes namespace where to install uyuni") cmd.Flags().String("helm-uyuni-chart", defaultChart, "URL to the uyuni helm chart") cmd.Flags().String("helm-uyuni-version", "", "Version of the uyuni helm chart") cmd.Flags().String("helm-uyuni-values", "", "Path to a values YAML file to use for Uyuni helm install") cmd.Flags().String("helm-certmanager-namespace", "cert-manager", "Kubernetes namespace where to install cert-manager") cmd.Flags().String("helm-certmanager-chart", "", "URL to the cert-manager helm chart. To be used for offline installations") cmd.Flags().String("helm-certmanager-version", "", "Version of the cert-manager helm chart") cmd.Flags().String("helm-certmanager-values", "", "Path to a values YAML file to use for cert-manager helm install") } // AddimageFlag add Image flags to a command. func AddImageFlag(cmd *cobra.Command) { cmd.Flags().String("image", defaultImage, "Image") cmd.Flags().String("tag", utils.DefaultTag, "Tag Image") utils.AddPullPolicyFlag(cmd) } // AddMigrationImageFlag add Migration Image flags to a command. func AddMigrationImageFlag(cmd *cobra.Command) { cmd.Flags().String("migration-image", "", "Migration image") cmd.Flags().String("migration-tag", utils.DefaultTag, "Migration image tag") } 0707010000008A000081B400000000000000000000000165DF469000001681000000000000000000000000000000000000002800000000uyuni-tools/mgradm/shared/utils/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "bytes" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/viper" "github.com/uyuni-project/uyuni-tools/mgradm/shared/templates" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // ExecCommand execute commands passed as argument in the current system. func ExecCommand(logLevel zerolog.Level, cnx *shared.Connection, args ...string) error { podName, err := cnx.GetPodName() if err != nil { return fmt.Errorf("execCommand failed %s", err) } commandArgs := []string{"exec", podName} command, err := cnx.GetCommand() if err != nil { log.Fatal().Err(err) } if command == "kubectl" { commandArgs = append(commandArgs, "-c", "uyuni", "--") } commandArgs = append(commandArgs, "sh", "-c", strings.Join(args, " ")) runCmd := exec.Command(command, commandArgs...) logger := utils.OutputLogWriter{Logger: log.Logger, LogLevel: logLevel} runCmd.Stdout = logger runCmd.Stderr = logger return runCmd.Run() } // GeneratePgsqlVersionUpgradeScript generates the PostgreSQL version upgrade script. func GeneratePgsqlVersionUpgradeScript(scriptDir string, oldPgVersion string, newPgVersion string, kubernetes bool) (string, error) { data := templates.PostgreSQLVersionUpgradeTemplateData{ OldVersion: oldPgVersion, NewVersion: newPgVersion, Kubernetes: kubernetes, } scriptName := "pgsqlVersionUpgrade.sh" scriptPath := filepath.Join(scriptDir, scriptName) if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { return "", fmt.Errorf("failed to generate %s", scriptName) } return scriptName, nil } // GenerateFinalizePostgresScript generates the script to finalize PostgreSQL upgrade. func GenerateFinalizePostgresScript(scriptDir string, RunAutotune bool, RunReindex bool, RunSchemaUpdate bool, RunDistroMigration bool, kubernetes bool) (string, error) { data := templates.FinalizePostgresTemplateData{ RunAutotune: RunAutotune, RunReindex: RunReindex, RunSchemaUpdate: RunSchemaUpdate, RunDistroMigration: RunDistroMigration, Kubernetes: kubernetes, } scriptName := "pgsqlFinalize.sh" scriptPath := filepath.Join(scriptDir, scriptName) if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { return "", fmt.Errorf("failed to generate %s", scriptName) } return scriptName, nil } // GeneratePostUpgradeScript generates the script to be run after upgrade. func GeneratePostUpgradeScript(scriptDir string, cobblerHost string) (string, error) { data := templates.PostUpgradeTemplateData{ CobblerHost: cobblerHost, } scriptName := "postUpgrade.sh" scriptPath := filepath.Join(scriptDir, scriptName) if err := utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { return "", fmt.Errorf("failed to generate %s", scriptName) } return scriptName, nil } // ReadContainerData returns values used to perform migration. func ReadContainerData(scriptDir string) (string, string, string, error) { data, err := os.ReadFile(filepath.Join(scriptDir, "data")) if err != nil { return "", "", "", errors.New("failed to read data extracted from source host") } viper.SetConfigType("env") if err := viper.ReadConfig(bytes.NewBuffer(data)); err != nil { return "", "", "", fmt.Errorf("cannot read config: %s", err) } return viper.GetString("Timezone"), viper.GetString("old_pg_version"), viper.GetString("new_pg_version"), nil } // RunMigration execute the migration script. func RunMigration(cnx *shared.Connection, tmpPath string, scriptName string) error { log.Info().Msg("Migrating server") err := ExecCommand(zerolog.InfoLevel, cnx, "/var/lib/uyuni-tools/"+scriptName) if err != nil { return fmt.Errorf("error running the migration script: %s", err) } return nil } // GenerateMigrationScript generates the script that perform migration. func GenerateMigrationScript(sourceFqdn string, kubernetes bool) (string, error) { scriptDir, err := os.MkdirTemp("", "mgradm-*") if err != nil { return "", fmt.Errorf("failed to create temporary directory: %s", err) } data := templates.MigrateScriptTemplateData{ Volumes: utils.ServerVolumeMounts, SourceFqdn: sourceFqdn, Kubernetes: kubernetes, } scriptPath := filepath.Join(scriptDir, "migrate.sh") if err = utils.WriteTemplateToFile(data, scriptPath, 0555, true); err != nil { return "", fmt.Errorf("failed to generate migration script: %s", err) } return scriptDir, nil } // RunningImage returns the image running in the current system. func RunningImage(cnx *shared.Connection, containerName string) (string, error) { command, err := cnx.GetCommand() switch command { case "podman": args := []string{"ps", "--format", "{{.Image}}", "--noheading"} image, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", args...) if err != nil { return "", err } return strings.Trim(string(image), "\n"), nil case "kubectl": //FIXME this will work until containers 0 is uyuni. Then jsonpath should be something like // {.items[0].spec.containers[?(@.name=="` + containerName + `")].image but there are problems // using RunCmdOutput with an arguments with round brackets args := []string{"get", "pods", kubernetes.ServerFilter, "-o", "jsonpath={.items[0].spec.containers[0].image}"} image, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", args...) log.Info().Msgf("image is: %s", image) if err != nil { return "", err } return strings.Trim(string(image), "\n"), nil } return command, err } 0707010000008B000041FD00000000000000000000000365DF469000000000000000000000000000000000000000000000001300000000uyuni-tools/mgrctl0707010000008C000041FD00000000000000000000000665DF469000000000000000000000000000000000000000000000001700000000uyuni-tools/mgrctl/cmd0707010000008D000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001B00000000uyuni-tools/mgrctl/cmd/api0707010000008E000081B400000000000000000000000165DF46900000066E000000000000000000000000000000000000002200000000uyuni-tools/mgrctl/cmd/api/api.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package api import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type apiFlags struct { api.ConnectionDetails `mapstructure:"api"` } // NewCommand generates a JSON over HTTP API helper tool command. func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) { var flags apiFlags apiCmd := &cobra.Command{ Use: "api", Short: "JSON over HTTP API helper tool", } apiGet := &cobra.Command{ Use: "get path [parameters]...", Short: "Call API GET request", Long: "Takes an API path and optional parameters and then issues GET request with the specified path and parameters. If user and password are provided, calls login before API call", RunE: func(cmd *cobra.Command, args []string) error { return utils.CommandHelper(globalFlags, cmd, args, &flags, runGet) }, } apiPost := &cobra.Command{ Use: "post path parameters...", Short: "Call API POST request", Long: "Takes an API path and parameters and then issues POST request with the specified path and parameters. User and password are mandatory. Parameters can be either JSON encoded string or one or more key=value pairs.", RunE: func(cmd *cobra.Command, args []string) error { return utils.CommandHelper(globalFlags, cmd, args, &flags, runPost) }, } apiCmd.AddCommand(apiGet) apiCmd.AddCommand(apiPost) if err := api.AddAPIFlags(apiCmd, false); err != nil { return apiCmd, err } return apiCmd, nil } 0707010000008F000081B400000000000000000000000165DF469000000426000000000000000000000000000000000000002200000000uyuni-tools/mgrctl/cmd/api/get.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package api import ( "encoding/json" "fmt" "strings" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/types" ) func runGet(globalFlags *types.GlobalFlags, flags *apiFlags, cmd *cobra.Command, args []string) error { log.Debug().Msgf("Running GET command %s", args[0]) client, err := api.Init(&flags.ConnectionDetails) if err != nil { return fmt.Errorf("unable to login to the server: %s", err) } path := args[0] options := args[1:] res, err := api.Get[interface{}](client, fmt.Sprintf("%s?%s", path, strings.Join(options, "&"))) if err != nil { return fmt.Errorf("error in query %s: %s", path, err) } // TODO do this only when result is JSON or TEXT. Watchout for binary data // Decode JSON to the string and pretty print it out, err := json.MarshalIndent(res.Result, "", " ") if err != nil { return err } fmt.Print(string(out)) return nil } 07070100000090000081B400000000000000000000000165DF469000000573000000000000000000000000000000000000002300000000uyuni-tools/mgrctl/cmd/api/post.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package api import ( "encoding/json" "fmt" "strings" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/types" ) func runPost(globalFlags *types.GlobalFlags, flags *apiFlags, cmd *cobra.Command, args []string) error { log.Debug().Msgf("Running POST command %s", args[0]) client, err := api.Init(&flags.ConnectionDetails) if err != nil { log.Fatal().Err(err).Msg("Unable to login to the server") } path := args[0] options := args[1:] var data map[string]interface{} if len(options) > 1 { log.Debug().Msg("Multiple options specified, assuming non JSON data") data = map[string]interface{}{} for _, o := range options { s := strings.SplitN(o, "=", 2) data[s[0]] = s[1] } } else { if err := json.NewDecoder(strings.NewReader(args[1])).Decode(&data); err != nil { log.Debug().Msg("Failed to decode parameters as JSON, assuming key=value pairs") } } res, err := api.Post[interface{}](client, path, data) if err != nil { return fmt.Errorf("error in query %s: %s", path, err) } if !res.Success { log.Error().Msg(res.Message) } out, err := json.MarshalIndent(res.Result, "", " ") if err != nil { log.Fatal().Err(err) } fmt.Print(string(out)) return nil } 07070100000091000081B400000000000000000000000165DF4690000008D6000000000000000000000000000000000000001E00000000uyuni-tools/mgrctl/cmd/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package cmd import ( "os" "path" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrctl/cmd/api" "github.com/uyuni-project/uyuni-tools/mgrctl/cmd/cp" "github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec" "github.com/uyuni-project/uyuni-tools/mgrctl/cmd/org" "github.com/uyuni-project/uyuni-tools/shared/completion" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // NewCommand returns a new cobra.Command implementing the root command for kinder. func NewUyunictlCommand() (*cobra.Command, error) { globalFlags := &types.GlobalFlags{} name := path.Base(os.Args[0]) rootCmd := &cobra.Command{ Use: name, Short: "Uyuni control tool", Long: "Uyuni control tool used to help user managing Uyuni and SUSE Manager Servers mainly through its API", Version: utils.Version, SilenceUsage: true, // Don't show usage help on errors } usage, err := utils.GetUsageWithConfigHelpTemplate(rootCmd.UsageTemplate()) if err != nil { return rootCmd, err } rootCmd.SetUsageTemplate(usage) rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", "configuration file path") rootCmd.PersistentFlags().StringVar(&globalFlags.LogLevel, "logLevel", "", "application log level (trace|debug|info|warn|error|fatal|panic)") rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { utils.LogInit(cmd.Name() != "exec") utils.SetLogLevel(globalFlags.LogLevel) // do not log if running the completion cmd as the output is redirect to create a file to source if cmd.Name() != "completion" { log.Info().Msgf("Welcome to %s", name) log.Info().Msgf("Executing command: %s", cmd.Name()) } } //FIXME this is currently return an err apiCmd, _ := api.NewCommand(globalFlags) rootCmd.AddCommand(apiCmd) rootCmd.AddCommand(exec.NewCommand(globalFlags)) rootCmd.AddCommand(cp.NewCommand(globalFlags)) rootCmd.AddCommand(completion.NewCommand(globalFlags)) //FIXME this is currently return an err orgCmd, _ := org.NewCommand(globalFlags) rootCmd.AddCommand(orgCmd) return rootCmd, nil } 07070100000092000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001A00000000uyuni-tools/mgrctl/cmd/cp07070100000093000081B400000000000000000000000165DF46900000070B000000000000000000000000000000000000002000000000uyuni-tools/mgrctl/cmd/cp/cp.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package cp import ( "fmt" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type flagpole struct { User string Group string Backend string } // NewCommand copy file to and from the containers. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { flags := &flagpole{} cpCmd := &cobra.Command{ Use: "cp [path/to/source.file] [path/to/destination.file]", Short: "copy files to and from the containers", Long: `Takes a source and destination parameters. One of them can be prefixed with 'server:' to indicate the path is within the server pod.`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { viper, err := utils.ReadConfig(globalFlags.ConfigPath, cmd) if err != nil { return err } if err := viper.Unmarshal(&flags); err != nil { log.Error().Err(err).Msgf("Failed to unmarshall configuration") return fmt.Errorf("failed to unmarshall configuration: %s", err) } return run(flags, cmd, args) }, } cpCmd.Flags().String("user", "", "User or UID to set on the destination file") cpCmd.Flags().String("group", "susemanager", "Group or GID to set on the destination file") utils.AddBackendFlag(cpCmd) return cpCmd } func run(flags *flagpole, cmd *cobra.Command, args []string) error { cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter) return cnx.Copy(args[0], args[1], flags.User, flags.Group) } 07070100000094000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001C00000000uyuni-tools/mgrctl/cmd/exec07070100000095000081B400000000000000000000000165DF469000000F4C000000000000000000000000000000000000002400000000uyuni-tools/mgrctl/cmd/exec/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package exec import ( "fmt" "io" "os" "os/exec" "strings" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type flagpole struct { Envs []string `mapstructure:"env"` Interactive bool Tty bool Backend string } // NewCommand returns a new cobra.Command for exec. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { var flags flagpole execCmd := &cobra.Command{ Use: "exec '[command-to-run --with-args]'", Short: "Execute commands inside the uyuni containers using 'sh -c'", RunE: func(cmd *cobra.Command, args []string) error { return utils.CommandHelper(globalFlags, cmd, args, &flags, run) }, } execCmd.SetUsageTemplate(execCmd.UsageTemplate()) execCmd.Flags().StringSliceP("env", "e", []string{}, "environment variables to pass to the command, separated by commas") execCmd.Flags().BoolP("interactive", "i", false, "Pass stdin to the container") execCmd.Flags().BoolP("tty", "t", false, "Stdin is a TTY") utils.AddBackendFlag(execCmd) return execCmd } func run(globalFlags *types.GlobalFlags, flags *flagpole, cmd *cobra.Command, args []string) error { cnx := shared.NewConnection(flags.Backend, podman.ServerContainerName, kubernetes.ServerFilter) podName, err := cnx.GetPodName() if err != nil { log.Fatal().Err(err) } command, err := cnx.GetCommand() if err != nil { log.Fatal().Err(err) } commandArgs := []string{"exec"} envs := []string{} envs = append(envs, flags.Envs...) if flags.Interactive { commandArgs = append(commandArgs, "-i") envs = append(envs, "ENV=/etc/sh.shrc.local") } if flags.Tty { commandArgs = append(commandArgs, "-t") envs = append(envs, "TERM") } commandArgs = append(commandArgs, podName) if command == "kubectl" { commandArgs = append(commandArgs, "-c", "uyuni", "--") } newEnv := []string{} for _, envValue := range envs { if !strings.Contains(envValue, "=") { if value, set := os.LookupEnv(envValue); set { newEnv = append(newEnv, fmt.Sprintf("%s=%s", envValue, value)) } } else { newEnv = append(newEnv, envValue) } } if len(newEnv) > 0 { commandArgs = append(commandArgs, "env") commandArgs = append(commandArgs, newEnv...) } commandArgs = append(commandArgs, "sh", "-c", strings.Join(args, " ")) err = RunRawCmd(command, commandArgs) if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { log.Info().Err(err).Msg("Command failed") os.Exit(exitErr.ExitCode()) } } log.Info().Msg("Command returned with exit code 0") return nil } type copyWriter struct { Stream io.Writer } // Write writes an array of buffer in a stream. func (l copyWriter) Write(p []byte) (n int, err error) { // Filter out kubectl line about terminated exit code if !strings.HasPrefix(string(p), "command terminated with exit code") { if _, err := l.Stream.Write(p); err != nil { return 0, fmt.Errorf("cannot write: %s", err) } n = len(p) if n > 0 && p[n-1] == '\n' { // Trim CR added by stdlog. p = p[0 : n-1] } log.Debug().Msg(string(p)) } return } // RunRawCmd runs a command, mapping stdout and start error, waiting and checking return code. func RunRawCmd(command string, args []string) error { log.Info().Msgf("Running: %s %s", command, strings.Join(args, " ")) runCmd := exec.Command(command, args...) runCmd.Stdin = os.Stdin runCmd.Stdout = copyWriter{Stream: os.Stdout} runCmd.Stderr = copyWriter{Stream: os.Stderr} if err := runCmd.Start(); err != nil { log.Debug().Err(err).Msg("error starting command") return err } return runCmd.Wait() } 07070100000096000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001B00000000uyuni-tools/mgrctl/cmd/org07070100000097000081B400000000000000000000000165DF4690000006F4000000000000000000000000000000000000002A00000000uyuni-tools/mgrctl/cmd/org/createFirst.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package org import ( "fmt" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/api/org" apiTypes "github.com/uyuni-project/uyuni-tools/shared/api/types" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type createFirstFlags struct { api.ConnectionDetails `mapstructure:"api"` Organization string Admin apiTypes.User } func createFirstCommand(globalFlags *types.GlobalFlags) *cobra.Command { cmd := &cobra.Command{ Use: "createFirst", Short: "create the first user and organization", RunE: func(cmd *cobra.Command, args []string) error { var flags createFirstFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, createFirst) }, } cmd.Flags().String("admin-login", "admin", "Administrator user name") cmd.Flags().String("admin-password", "", "Administrator password. If empty, the first user will not be created") cmd.Flags().String("admin-firstName", "Administrator", "The first name of the administrator") cmd.Flags().String("admin-lastName", "McAdmin", "The last name of the administrator") cmd.Flags().String("admin-email", "root@localhost", "The administrator's email") cmd.Flags().String("organization", "Organiszation", "The first organization name") return cmd } func createFirst(globalFlags *types.GlobalFlags, flags *createFirstFlags, cmd *cobra.Command, args []string) error { org, err := org.CreateFirst(&flags.ConnectionDetails, flags.Organization, &flags.Admin) if err != nil { return err } fmt.Printf("Organization %s created with id %d", org.Name, org.Id) return nil } 07070100000098000081B400000000000000000000000165DF46900000024F000000000000000000000000000000000000002200000000uyuni-tools/mgrctl/cmd/org/org.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package org import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand command for APIs. func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) { orgCmd := &cobra.Command{ Use: "org", Short: "Organization-related commands", } if err := api.AddAPIFlags(orgCmd, false); err != nil { return orgCmd, err } orgCmd.AddCommand(createFirstCommand(globalFlags)) return orgCmd, nil } 07070100000099000081B400000000000000000000000165DF469000000181000000000000000000000000000000000000001B00000000uyuni-tools/mgrctl/main.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package main import ( "os" "github.com/uyuni-project/uyuni-tools/mgrctl/cmd" ) // Run runs the `mgrctl` root command. func Run() error { run, err := cmd.NewUyunictlCommand() if err != nil { return err } return run.Execute() } func main() { if err := Run(); err != nil { os.Exit(1) } } 0707010000009A000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000001300000000uyuni-tools/mgrpxy0707010000009B000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000001700000000uyuni-tools/mgrpxy/cmd0707010000009C000081B400000000000000000000000165DF4690000007F1000000000000000000000000000000000000001E00000000uyuni-tools/mgrpxy/cmd/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package cmd import ( "os" "path" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/install" "github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/uninstall" "github.com/uyuni-project/uyuni-tools/shared/completion" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // NewCommand returns a new cobra.Command implementing the root command for kinder. func NewUyuniproxyCommand() (*cobra.Command, error) { globalFlags := &types.GlobalFlags{} name := path.Base(os.Args[0]) rootCmd := &cobra.Command{ Use: name, Short: "Uyuni proxy administration tool", Long: "Uyuni tool used to help user administer uyuni proxies on kubernetes and podman", Version: utils.Version, SilenceUsage: true, // Don't show usage help on errors } usage, err := utils.GetUsageWithConfigHelpTemplate(rootCmd.UsageTemplate()) if err != nil { return rootCmd, err } rootCmd.SetUsageTemplate(usage) rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { utils.LogInit(true) utils.SetLogLevel(globalFlags.LogLevel) // do not log if running the completion cmd as the output is redirected to create a file to source if cmd.Name() != "completion" { log.Info().Msgf("Welcome to %s", name) log.Info().Msgf("Executing command: %s", cmd.Name()) } } rootCmd.PersistentFlags().StringVarP(&globalFlags.ConfigPath, "config", "c", "", "configuration file path") rootCmd.PersistentFlags().StringVar(&globalFlags.LogLevel, "logLevel", "", "application log level (trace|debug|info|warn|error|fatal|panic)") installCmd := install.NewCommand(globalFlags) rootCmd.AddCommand(installCmd) uninstallCmd, err := uninstall.NewCommand(globalFlags) if err != nil { return rootCmd, err } rootCmd.AddCommand(uninstallCmd) rootCmd.AddCommand(completion.NewCommand(globalFlags)) return rootCmd, nil } 0707010000009D000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000001F00000000uyuni-tools/mgrpxy/cmd/install0707010000009E000081B400000000000000000000000165DF4690000002D9000000000000000000000000000000000000002A00000000uyuni-tools/mgrpxy/cmd/install/install.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package install import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/install/kubernetes" "github.com/uyuni-project/uyuni-tools/mgrpxy/cmd/install/podman" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand install a new proxy from scratch. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { installCmd := &cobra.Command{ Use: "install [fqdn]", Short: "install a new proxy from scratch", Long: "Install a new proxy from scratch", } installCmd.AddCommand(podman.NewCommand(globalFlags)) installCmd.AddCommand(kubernetes.NewCommand(globalFlags)) return installCmd } 0707010000009F000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002A00000000uyuni-tools/mgrpxy/cmd/install/kubernetes070701000000A0000081B400000000000000000000000165DF469000000574000000000000000000000000000000000000003800000000uyuni-tools/mgrpxy/cmd/install/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/kubernetes" pxy_utils "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) type kubernetesProxyInstallFlags struct { pxy_utils.ProxyInstallFlags `mapstructure:",squash"` Helm kubernetes.HelmFlags } // NewCommand install a new proxy on a running kubernetes cluster. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { cmd := &cobra.Command{ Use: "kubernetes [path/to/config.tar.gz]", Short: "install a new proxy on a running kubernetes cluster", Long: `Install a new proxy on a running kubernetes cluster. It only takes the path to the configuration tarball generated by the server as parameter. The install kubernetes command assumes kubectl is installed locally. NOTE: for now installing on a remote kubernetes cluster is not supported! `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var flags kubernetesProxyInstallFlags return utils.CommandHelper(globalFlags, cmd, args, &flags, installForKubernetes) }, } pxy_utils.AddInstallFlags(cmd) kubernetes.AddHelmFlags(cmd) return cmd } 070701000000A1000081B400000000000000000000000165DF46900000078B000000000000000000000000000000000000003300000000uyuni-tools/mgrpxy/cmd/install/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "fmt" "os" "os/exec" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils" shared_kubernetes "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/types" shared_utils "github.com/uyuni-project/uyuni-tools/shared/utils" ) func installForKubernetes(globalFlags *types.GlobalFlags, flags *kubernetesProxyInstallFlags, cmd *cobra.Command, args []string, ) error { for _, binary := range []string{"kubectl", "helm"} { if _, err := exec.LookPath(binary); err != nil { return fmt.Errorf("install %s before running this command", binary) } } // Unpack the tarball configPath := utils.GetConfigPath(args) tmpDir, err := os.MkdirTemp("", "mgrpxy-*") if err != nil { return fmt.Errorf("failed to create temporary directory") } defer os.RemoveAll(tmpDir) if err := shared_utils.ExtractTarGz(configPath, tmpDir); err != nil { return fmt.Errorf("failed to extract configuration") } // Check the kubernetes cluster setup clusterInfos := shared_kubernetes.CheckCluster() // If installing on k3s, install the traefik helm config in manifests isK3s := clusterInfos.IsK3s() IsRke2 := clusterInfos.IsRke2() if isK3s { shared_kubernetes.InstallK3sTraefikConfig(shared_utils.PROXY_TCP_PORTS, shared_utils.UDP_PORTS) } else if IsRke2 { shared_kubernetes.InstallRke2NginxConfig(shared_utils.PROXY_TCP_PORTS, shared_utils.UDP_PORTS, flags.Helm.Proxy.Namespace) } // Install the uyuni proxy helm chart if err := kubernetes.Deploy(&flags.ProxyInstallFlags, &flags.Helm, tmpDir, clusterInfos.GetKubeconfig(), "--set", "ingress="+clusterInfos.Ingress); err != nil { return fmt.Errorf("cannot deploy proxy helm chart: %s", err) } return nil } 070701000000A2000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002600000000uyuni-tools/mgrpxy/cmd/install/podman070701000000A3000081B400000000000000000000000165DF46900000053F000000000000000000000000000000000000003000000000uyuni-tools/mgrpxy/cmd/install/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" shared_utils "github.com/uyuni-project/uyuni-tools/shared/utils" ) type podmanProxyInstallFlags struct { utils.ProxyInstallFlags `mapstructure:",squash"` Podman podman.PodmanFlags } // NewCommand install a new proxy on podman from scratch. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { podmanCmd := &cobra.Command{ Use: "podman [path/to/config.tar.gz]", Short: "install a new proxy on podman from scratch", Long: `Install a new proxy on podman from scratch It only takes the path to the configuration tarball generated by the server as parameter. The install podman command assumes podman is installed locally. NOTE: for now installing on a remote podman is not supported! `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var flags podmanProxyInstallFlags return shared_utils.CommandHelper(globalFlags, cmd, args, &flags, installForPodman) }, } utils.AddInstallFlags(podmanCmd) podman.AddPodmanInstallFlag(podmanCmd) return podmanCmd } 070701000000A4000081B400000000000000000000000165DF4690000009FD000000000000000000000000000000000000002F00000000uyuni-tools/mgrpxy/cmd/install/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os" "os/exec" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/podman" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils" shared_podman "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" shared_utils "github.com/uyuni-project/uyuni-tools/shared/utils" ) // Start the proxy services. func startPod() error { ret, err := shared_podman.IsServiceRunning(shared_podman.ProxyService) if err != nil { return err } if ret { return shared_podman.RestartService(shared_podman.ProxyService) } else { return shared_podman.EnableService(shared_podman.ProxyService) } } func installForPodman(globalFlags *types.GlobalFlags, flags *podmanProxyInstallFlags, cmd *cobra.Command, args []string) error { if _, err := exec.LookPath("podman"); err != nil { return fmt.Errorf("install podman before running this command") } configPath := utils.GetConfigPath(args) if err := unpackConfig(configPath); err != nil { return fmt.Errorf("failed to extract proxy config from %s file: %s", configPath, err) } httpdImage, err := getContainerImage(flags, "httpd") if err != nil { return err } saltBrokerImage, err := getContainerImage(flags, "salt-broker") if err != nil { return err } squidImage, err := getContainerImage(flags, "squid") if err != nil { return err } sshImage, err := getContainerImage(flags, "ssh") if err != nil { return err } tftpdImage, err := getContainerImage(flags, "tftpd") if err != nil { return err } // Setup the systemd service configuration options if err := podman.GenerateSystemdService(httpdImage, saltBrokerImage, squidImage, sshImage, tftpdImage, flags.Podman.Args); err != nil { return fmt.Errorf("cannot generate systemd file: %s", err) } return startPod() } func getContainerImage(flags *podmanProxyInstallFlags, name string) (string, error) { image := flags.GetContainerImage(name) err := shared_podman.PrepareImage(image, flags.PullPolicy) if err != nil { return "", err } return image, nil } func unpackConfig(configPath string) error { log.Info().Msgf("Setting up proxy with configuration %s", configPath) const proxyConfigDir = "/etc/uyuni/proxy" if err := os.MkdirAll(proxyConfigDir, 0755); err != nil { return err } if err := shared_utils.ExtractTarGz(configPath, proxyConfigDir); err != nil { return err } return nil } 070701000000A5000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002100000000uyuni-tools/mgrpxy/cmd/uninstall070701000000A6000081B400000000000000000000000165DF4690000003E2000000000000000000000000000000000000002F00000000uyuni-tools/mgrpxy/cmd/uninstall/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package uninstall import ( "github.com/uyuni-project/uyuni-tools/shared/kubernetes" ) func uninstallForKubernetes(dryRun bool) { clusterInfos := kubernetes.CheckCluster() kubeconfig := clusterInfos.GetKubeconfig() // TODO Find all the PVs related to the server if we want to delete them // Uninstall uyuni kubernetes.HelmUninstall(kubeconfig, "uyuni-proxy", "", dryRun) // TODO Remove the PVs or wait for their automatic removal if purge is requested // Also wait if the PVs are dynamic with Delete reclaim policy but the user didn't ask to purge them // Since some storage plugins don't handle Delete policy, we may need to check for error events to avoid infinite loop // Remove the K3s Traefik config if clusterInfos.IsK3s() { kubernetes.UninstallK3sTraefikConfig(dryRun) } // Remove the rke2 nginx config if clusterInfos.IsRke2() { kubernetes.UninstallRke2NginxConfig(dryRun) } } 070701000000A7000081B400000000000000000000000165DF4690000004C6000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/cmd/uninstall/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package uninstall import ( "fmt" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/utils" ) func uninstallForPodman(dryRun bool, purge bool) error { // Uninstall the service podman.UninstallService("uyuni-proxy-pod", dryRun) // Force stop the pod for _, containerName := range podman.ProxyContainerNames { podman.DeleteContainer(containerName, dryRun) } // Remove the volumes if purge { // Merge all proxy containers volumes into a map volumes := map[string]string{} allProxyVolumes := []map[string]string{ utils.PROXY_HTTPD_VOLUMES, utils.PROXY_SQUID_VOLUMES, utils.PROXY_TFTPD_VOLUMES, } for _, volumesList := range allProxyVolumes { for volume, mount := range volumesList { volumes[volume] = mount } } // Delete each volume for volume := range volumes { if err := podman.DeleteVolume(volume, dryRun); err != nil { return fmt.Errorf("cannot delete volume %s: %s", volume, err) } } log.Info().Msg("All volumes removed") } podman.DeleteNetwork(dryRun) return podman.ReloadDaemon(dryRun) } 070701000000A8000081B400000000000000000000000165DF4690000006BA000000000000000000000000000000000000002E00000000uyuni-tools/mgrpxy/cmd/uninstall/uninstall.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package uninstall import ( "fmt" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // NewCommand for uninstall proxy. func NewCommand(globalFlags *types.GlobalFlags) (*cobra.Command, error) { uninstallCmd := &cobra.Command{ Use: "uninstall", Short: "uninstall a proxy", Long: "Uninstall a proxy and optionally the corresponding volumes." + kubernetes.UninstallHelp, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { dryRun, _ := cmd.Flags().GetBool("dry-run") purge, _ := cmd.Flags().GetBool("purge-volumes") backend, _ := cmd.Flags().GetString("backend") if len(backend) <= 0 { backend = "podman" } cnx := shared.NewConnection(backend, podman.ProxyContainerNames[0], kubernetes.ProxyFilter) command, err := cnx.GetCommand() if err != nil { log.Fatal().Err(err).Msg("Failed to determine suitable backend") } switch command { case "podman": if err := uninstallForPodman(dryRun, purge); err != nil { return fmt.Errorf("cannot uninstall podman: %s", err) } case "kubectl": uninstallForKubernetes(dryRun) } return nil }, } uninstallCmd.Flags().BoolP("dry-run", "n", false, "Only show what would be done") uninstallCmd.Flags().Bool("purge-volumes", false, "Also remove the volume") utils.AddBackendFlag(uninstallCmd) return uninstallCmd, nil } 070701000000A9000081B400000000000000000000000165DF469000000183000000000000000000000000000000000000001B00000000uyuni-tools/mgrpxy/main.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package main import ( "os" "github.com/uyuni-project/uyuni-tools/mgrpxy/cmd" ) // Run runs the `mgrpxy` root command. func Run() error { run, err := cmd.NewUyuniproxyCommand() if err != nil { return err } return run.Execute() } func main() { if err := Run(); err != nil { os.Exit(1) } } 070701000000AA000041FD00000000000000000000000665DF469000000000000000000000000000000000000000000000001A00000000uyuni-tools/mgrpxy/shared070701000000AB000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002500000000uyuni-tools/mgrpxy/shared/kubernetes070701000000AC000081B400000000000000000000000165DF46900000037D000000000000000000000000000000000000002C00000000uyuni-tools/mgrpxy/shared/kubernetes/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "fmt" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // HelmFlags it's used for helm chart flags. type HelmFlags struct { Proxy types.ChartFlags } // AddHelmFlags add helm flags to a command. func AddHelmFlags(cmd *cobra.Command) { defaultChart := fmt.Sprintf("oci://%s/proxy-helm", utils.DefaultNamespace) cmd.Flags().String("helm-proxy-namespace", "default", "Kubernetes namespace where to install the proxy") cmd.Flags().String("helm-proxy-chart", defaultChart, "URL to the proxy helm chart") cmd.Flags().String("helm-proxy-version", "", "Version of the proxy helm chart") cmd.Flags().String("helm-proxy-values", "", "Path to a values YAML file to use for proxy helm install") } 070701000000AD000081B400000000000000000000000165DF469000000750000000000000000000000000000000000000002F00000000uyuni-tools/mgrpxy/shared/kubernetes/deploy.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "fmt" "path" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/utils" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" ) const helmAppName = "uyuni-proxy" // Deploy will deploy proxy in kubernetes. func Deploy(installFlags *utils.ProxyInstallFlags, helmFlags *HelmFlags, configDir string, kubeconfig string, helmArgs ...string, ) error { log.Info().Msg("Installing Uyuni") helmParams := []string{} // Pass the user-provided values file extraValues := helmFlags.Proxy.Values if extraValues != "" { helmParams = append(helmParams, "-f", extraValues) } helmParams = append(helmParams, "-f", path.Join(configDir, "httpd.yaml"), "-f", path.Join(configDir, "ssh.yaml"), "-f", path.Join(configDir, "config.yaml"), "--set", "images.proxy-httpd="+installFlags.GetContainerImage("httpd"), "--set", "images.proxy-salt-broker="+installFlags.GetContainerImage("salt-broker"), "--set", "images.proxy-squid="+installFlags.GetContainerImage("squid"), "--set", "images.proxy-ssh="+installFlags.GetContainerImage("ssh"), "--set", "images.proxy-tftpd="+installFlags.GetContainerImage("tftpd"), "--set", "repository="+installFlags.ImagesLocation, "--set", "version="+installFlags.Tag, "--set", "pullPolicy="+kubernetes.GetPullPolicy(installFlags.PullPolicy)) helmParams = append(helmParams, helmArgs...) // Install the helm chart if err := kubernetes.HelmUpgrade(kubeconfig, helmFlags.Proxy.Namespace, true, "", helmAppName, helmFlags.Proxy.Chart, helmFlags.Proxy.Version, helmParams...); err != nil { return fmt.Errorf("cannot run helm upgrade: %s", err) } // Wait for the pod to be started return kubernetes.WaitForDeployment(helmFlags.Proxy.Namespace, helmAppName, "uyuni-proxy") } 070701000000AE000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002100000000uyuni-tools/mgrpxy/shared/podman070701000000AF000081B400000000000000000000000165DF469000000CFA000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/shared/podman/podman.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "path" "strings" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/mgrpxy/shared/templates" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // GenerateSystemdService generates all the systemd files required by proxy. func GenerateSystemdService(httpdImage string, saltBrokerImage string, squidImage string, sshImage string, tftpdImage string, podmanArgs []string) error { if err := podman.SetupNetwork(); err != nil { return fmt.Errorf("cannot setup network: %s", err) } log.Info().Msg("Generating systemd services") httpProxyConfig := getHttpProxyConfig() ports := []types.PortMap{} ports = append(ports, utils.PROXY_TCP_PORTS...) ports = append(ports, utils.PROXY_PODMAN_PORTS...) ports = append(ports, utils.UDP_PORTS...) // Pod dataPod := templates.PodTemplateData{ Ports: ports, HttpProxyFile: httpProxyConfig, Args: strings.Join(podmanArgs, " "), } if err := generateSystemdFile(dataPod, "pod"); err != nil { return fmt.Errorf("cannot generated systemd file: %s", err) } // Httpd dataHttpd := templates.HttpdTemplateData{ Volumes: utils.PROXY_HTTPD_VOLUMES, HttpProxyFile: httpProxyConfig, Image: httpdImage, } if err := generateSystemdFile(dataHttpd, "httpd"); err != nil { return fmt.Errorf("cannot generated systemd file: %s", err) } // Salt broker dataSaltBroker := templates.SaltBrokerTemplateData{ HttpProxyFile: httpProxyConfig, Image: saltBrokerImage, } if err := generateSystemdFile(dataSaltBroker, "salt-broker"); err != nil { return fmt.Errorf("cannot generated systemd file: %s", err) } // Squid dataSquid := templates.SquidTemplateData{ Volumes: utils.PROXY_SQUID_VOLUMES, HttpProxyFile: httpProxyConfig, Image: squidImage, } if err := generateSystemdFile(dataSquid, "squid"); err != nil { return fmt.Errorf("cannot generated systemd file: %s", err) } // SSH dataSSH := templates.SSHTemplateData{ HttpProxyFile: httpProxyConfig, Image: sshImage, } if err := generateSystemdFile(dataSSH, "ssh"); err != nil { return fmt.Errorf("cannot generated systemd file: %s", err) } // Tftpd dataTftpd := templates.TFTPDTemplateData{ Volumes: utils.PROXY_TFTPD_VOLUMES, HttpProxyFile: httpProxyConfig, Image: tftpdImage, } if err := generateSystemdFile(dataTftpd, "tftpd"); err != nil { return fmt.Errorf("cannot generated systemd file: %s", err) } return podman.ReloadDaemon(false) } func generateSystemdFile(template utils.Template, service string) error { name := fmt.Sprintf("uyuni-proxy-%s.service", service) const systemdPath = "/etc/systemd/system" path := path.Join(systemdPath, name) if err := utils.WriteTemplateToFile(template, path, 0555, true); err != nil { return fmt.Errorf("failed to generate %s", path) } return nil } func getHttpProxyConfig() string { const httpProxyConfigPath = "/etc/sysconfig/proxy" // Only SUSE distros seem to have such a file for HTTP proxy settings if utils.FileExists(httpProxyConfigPath) { return httpProxyConfigPath } return "" } 070701000000B0000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002400000000uyuni-tools/mgrpxy/shared/templates070701000000B1000081B400000000000000000000000165DF4690000006CE000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/shared/templates/httpd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const httpdTemplate = `# uyuni-proxy-httpd.service, generated by mgrpxy # Use an uyuni-proxy-httpd.service.d/local.conf file to override [Unit] Description=Uyuni proxy httpd container service Wants=network.target After=network-online.target BindsTo=uyuni-proxy-pod.service After=uyuni-proxy-pod.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n Environment=UYUNI_IMAGE={{ .Image }} {{- if .HttpProxyFile }} EnvironmentFile={{ .HttpProxyFile }} {{- end }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-proxy-httpd.pid %t/uyuni-proxy-httpd.ctr-id ExecStart=/usr/bin/podman run \ --conmon-pidfile %t/uyuni-proxy-httpd.pid \ --cidfile %t/uyuni-proxy-httpd.ctr-id \ --cgroups=no-conmon \ --pod-id-file %t/uyuni-proxy-pod.pod-id -d \ --replace -dt \ -v /etc/uyuni/proxy:/etc/uyuni:ro \ {{- range $name, $path := .Volumes }} -v {{ $name }}:{{ $path }} \ {{- end }} --name uyuni-proxy-httpd \ ${UYUNI_IMAGE} ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-httpd.ctr-id -t 10 ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-httpd.ctr-id PIDFile=%t/uyuni-proxy-httpd.pid TimeoutStopSec=60 Type=forking [Install] WantedBy=multi-user.target default.target ` // HttpdTemplateData represents HTTPD information to create systemd file. type HttpdTemplateData struct { Volumes map[string]string HttpProxyFile string Image string } // Render will create the systemd configuration file. func (data HttpdTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(httpdTemplate)) return t.Execute(wr, data) } 070701000000B2000081B400000000000000000000000165DF469000000776000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/shared/templates/pod.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" "github.com/uyuni-project/uyuni-tools/shared/types" ) const podTemplate = `# uyuni-proxy-pod.service, generated by mgrpxy [Unit] Description=Podman uyuni-proxy-pod.service Wants=network.target After=network-online.target Requires=uyuni-proxy-httpd.service uyuni-proxy-salt-broker.service uyuni-proxy-squid.service uyuni-proxy-ssh.service uyuni-proxy-tftpd.service Before=uyuni-proxy-httpd.service uyuni-proxy-salt-broker.service uyuni-proxy-squid.service uyuni-proxy-ssh.service uyuni-proxy-tftpd.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n {{- if .HttpProxyFile }} EnvironmentFile={{ .HttpProxyFile }} {{- end }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-proxy-pod.pid %t/uyuni-proxy-pod.pod-id ExecStartPre=/usr/bin/podman pod create --infra-conmon-pidfile %t/uyuni-proxy-pod.pid \ --pod-id-file %t/uyuni-proxy-pod.pod-id --name uyuni-proxy-pod \ {{- range .Ports }} -p {{ .Exposed }}:{{ .Port }}{{ if .Protocol }}/{{ .Protocol }}{{ end }} \ {{- end }} --replace {{ .Args }} ExecStart=/usr/bin/podman pod start --pod-id-file %t/uyuni-proxy-pod.pod-id ExecStop=/usr/bin/podman pod stop --ignore --pod-id-file %t/uyuni-proxy-pod.pod-id -t 10 ExecStopPost=/usr/bin/podman pod rm --ignore -f --pod-id-file %t/uyuni-proxy-pod.pod-id PIDFile=%t/uyuni-proxy-pod.pid TimeoutStopSec=60 Type=forking [Install] WantedBy=multi-user.target default.target ` // PodTemplateData POD information to create systemd file. type PodTemplateData struct { Ports []types.PortMap HttpProxyFile string Args string } // Render will create the systemd configuration file. func (data PodTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(podTemplate)) return t.Execute(wr, data) } 070701000000B3000081B400000000000000000000000165DF4690000006BD000000000000000000000000000000000000003300000000uyuni-tools/mgrpxy/shared/templates/salt-broker.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const saltBrokerTemplate = `# uyuni-proxy-salt-broker.service, generated by mgrpxy # Use an uyuni-proxy-salt-broker.service.d/local.conf file to override [Unit] Description=Uyuni proxy Salt broker container service Wants=network.target After=network-online.target BindsTo=uyuni-proxy-pod.service After=uyuni-proxy-pod.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n Environment=UYUNI_IMAGE={{ .Image }} {{- if .HttpProxyFile }} EnvironmentFile={{ .HttpProxyFile }} {{- end }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-proxy-salt-broker.pid %t/uyuni-proxy-salt-broker.ctr-id ExecStart=/usr/bin/podman run \ --conmon-pidfile %t/uyuni-proxy-salt-broker.pid \ --cidfile %t/uyuni-proxy-salt-broker.ctr-id \ --cgroups=no-conmon \ --pod-id-file %t/uyuni-proxy-pod.pod-id -d \ --replace -dt \ -v /etc/uyuni/proxy:/etc/uyuni:ro \ --name uyuni-proxy-salt-broker \ ${UYUNI_IMAGE} ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-salt-broker.ctr-id -t 10 ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-salt-broker.ctr-id PIDFile=%t/uyuni-proxy-salt-broker.pid TimeoutStopSec=60 Type=forking [Install] WantedBy=multi-user.target default.target ` // SaltBrokerTemplateData represents Salt Broker information to create systemd file. type SaltBrokerTemplateData struct { HttpProxyFile string Image string } // Render will create the systemd configuration file. func (data SaltBrokerTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(saltBrokerTemplate)) return t.Execute(wr, data) } 070701000000B4000081B400000000000000000000000165DF4690000006C3000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/shared/templates/squid.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const squidTemplate = `# uyuni-proxy-squid.service, generated by mgrpxy # Use an uyuni-proxy-squid.service.d/local.conf file to override [Unit] Description=Uyuni proxy squid container service Wants=network.target After=network-online.target BindsTo=uyuni-proxy-pod.service After=uyuni-proxy-pod.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n Environment=UYUNI_IMAGE={{ .Image }} {{- if .HttpProxyFile }} EnvironmentFile={{ .HttpProxyFile }} {{- end }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-proxy-squid.pid %t/uyuni-proxy-squid.ctr-id ExecStart=/usr/bin/podman run \ --conmon-pidfile %t/uyuni-proxy-squid.pid \ --cidfile %t/uyuni-proxy-squid.ctr-id \ --cgroups=no-conmon \ --pod-id-file %t/uyuni-proxy-pod.pod-id -d \ --replace -dt \ -v /etc/uyuni/proxy:/etc/uyuni:ro \ {{- range $name, $path := .Volumes }} -v {{ $name }}:{{ $path }} \ {{- end }} --name uyuni-proxy-squid \ ${UYUNI_IMAGE} ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-squid.ctr-id -t 10 ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-squid.ctr-id PIDFile=%t/uyuni-proxy-squid.pid TimeoutStopSec=60 Type=forking [Install] WantedBy=multi-user.target default.target ` // SquidTemplateData Squid information to create systemd file. type SquidTemplateData struct { Volumes map[string]string HttpProxyFile string Image string } // Render will create the systemd configuration file. func (data SquidTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(squidTemplate)) return t.Execute(wr, data) } 070701000000B5000081B400000000000000000000000165DF46900000062F000000000000000000000000000000000000002B00000000uyuni-tools/mgrpxy/shared/templates/ssh.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const sshTemplate = `# uyuni-proxy-ssh.service, generated by mgrpxy # Use an uyuni-proxy-ssh.service.d/local.conf file to override [Unit] Description=Uyuni proxy ssh container service Wants=network.target After=network-online.target BindsTo=uyuni-proxy-pod.service After=uyuni-proxy-pod.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n Environment=UYUNI_IMAGE={{ .Image }} {{- if .HttpProxyFile }} EnvironmentFile={{ .HttpProxyFile }} {{- end }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-proxy-ssh.pid %t/uyuni-proxy-ssh.ctr-id ExecStart=/usr/bin/podman run \ --conmon-pidfile %t/uyuni-proxy-ssh.pid \ --cidfile %t/uyuni-proxy-ssh.ctr-id \ --cgroups=no-conmon \ --pod-id-file %t/uyuni-proxy-pod.pod-id -d \ --replace -dt \ -v /etc/uyuni/proxy:/etc/uyuni:ro \ --name uyuni-proxy-ssh \ ${UYUNI_IMAGE} ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-ssh.ctr-id -t 10 ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-ssh.ctr-id PIDFile=%t/uyuni-proxy-ssh.pid TimeoutStopSec=60 Type=forking [Install] WantedBy=multi-user.target default.target ` // SSHTemplateData SSH information to create systemd file. type SSHTemplateData struct { HttpProxyFile string Image string } // Render will create the systemd configuration file. func (data SSHTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(sshTemplate)) return t.Execute(wr, data) } 070701000000B6000081B400000000000000000000000165DF4690000006E8000000000000000000000000000000000000002D00000000uyuni-tools/mgrpxy/shared/templates/tftpd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package templates import ( "io" "text/template" ) const tftpdTemplate = `# uyuni-proxy-tftpd.service, generated by mgrpxy # Use an uyuni-proxy-tftpd.service.d/local.conf file to override [Unit] Description=Uyuni proxy tftpd container service Wants=network.target After=network-online.target BindsTo=uyuni-proxy-pod.service After=uyuni-proxy-pod.service [Service] Environment=PODMAN_SYSTEMD_UNIT=%n Environment=UYUNI_IMAGE={{ .Image }} {{- if .HttpProxyFile }} EnvironmentFile={{ .HttpProxyFile }} {{- end }} Restart=on-failure ExecStartPre=/bin/rm -f %t/uyuni-proxy-tftpd.pid %t/uyuni-proxy-tftpd.ctr-id ExecStart=/usr/bin/podman run \ --conmon-pidfile %t/uyuni-proxy-tftpd.pid \ --cidfile %t/uyuni-proxy-tftpd.ctr-id \ --cgroups=no-conmon \ --pod-id-file %t/uyuni-proxy-pod.pod-id -d \ --replace -dt \ -v /etc/uyuni/proxy:/etc/uyuni:ro \ {{- range $name, $path := .Volumes }} -v {{ $name }}:{{ $path }} \ {{- end }} --name uyuni-proxy-tftpd \ ${UYUNI_IMAGE} ExecStop=/usr/bin/podman stop --ignore --cidfile %t/uyuni-proxy-tftpd.ctr-id -t 10 ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/uyuni-proxy-tftpd.ctr-id PIDFile=%t/uyuni-proxy-tftpd.pid TimeoutStopSec=60 Type=forking [Install] WantedBy=multi-user.target default.target ` // TFTPDTemplateData represents information used to create TFTPD systemd configuration file. type TFTPDTemplateData struct { Volumes map[string]string HttpProxyFile string Image string } // Render will create the TFTPD systemd configuration file. func (data TFTPDTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("service").Parse(tftpdTemplate)) return t.Execute(wr, data) } 070701000000B7000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000002000000000uyuni-tools/mgrpxy/shared/utils070701000000B8000081B400000000000000000000000165DF4690000001C1000000000000000000000000000000000000002700000000uyuni-tools/mgrpxy/shared/utils/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // GetConfigPath returns the configuration path if exists. func GetConfigPath(args []string) string { configPath := args[0] if !utils.FileExists(configPath) { log.Fatal().Msgf("argument is not an existing file: %s", configPath) } return configPath } 070701000000B9000081B400000000000000000000000165DF469000000A1B000000000000000000000000000000000000002900000000uyuni-tools/mgrpxy/shared/utils/flags.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "fmt" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // ProxyInstallFlags are the flags used by install proxy command. type ProxyInstallFlags struct { ImagesLocation string `mapstructure:"imagesLocation"` Tag string `namespace:"tag"` PullPolicy string `mapstructure:"pullPolicy"` Httpd types.ImageFlags `mapstructure:"httpd"` SaltBroker types.ImageFlags `mapstructure:"saltBroker"` Squid types.ImageFlags `mapstructure:"squid"` Ssh types.ImageFlags `mapstructure:"ssh"` Tftpd types.ImageFlags `mapstructure:"tftpd"` } // Get the full container image name and tag for a container name. func (f *ProxyInstallFlags) GetContainerImage(name string) string { imageName := "proxy-" + name image := fmt.Sprintf("%s/%s", f.ImagesLocation, imageName) tag := f.Tag var containerImage *types.ImageFlags switch name { case "httpd": containerImage = &f.Httpd case "salt-broker": containerImage = &f.SaltBroker case "squid": containerImage = &f.Squid case "ssh": containerImage = &f.Ssh case "tftpd": containerImage = &f.Tftpd default: log.Warn().Msgf("Invalid proxy container name: %s", name) } if containerImage != nil { if containerImage.Name != "" { image = containerImage.Name } if containerImage.Tag != "" { tag = containerImage.Tag } } imageUrl, err := utils.ComputeImage(image, tag) if err != nil { log.Fatal().Err(err).Msg("Failed to compute image URL") } return imageUrl } // AddInstallFlags will add the proxy install flags to a command. func AddInstallFlags(cmd *cobra.Command) { cmd.Flags().String("imagesLocation", utils.DefaultNamespace, "registry URL prefix containing the all the container images") cmd.Flags().String("tag", utils.DefaultTag, "Tag Image") utils.AddPullPolicyFlag(cmd) addContainerImageFlags(cmd, "httpd") addContainerImageFlags(cmd, "saltBroker") addContainerImageFlags(cmd, "squid") addContainerImageFlags(cmd, "ssh") addContainerImageFlags(cmd, "tftpd") } func addContainerImageFlags(cmd *cobra.Command, container string) { cmd.Flags().String(container+"-image", "", fmt.Sprintf("Image for %s container, overrides the namespace if set", container)) cmd.Flags().String(container+"-tag", "", fmt.Sprintf("Tag for %s container, overrides the global value if set", container)) } 070701000000BA000081B400000000000000000000000165DF469000000AD7000000000000000000000000000000000000001700000000uyuni-tools/modules.md<!-- SPDX-FileCopyrightText: 2023 SUSE LLC SPDX-License-Identifier: Apache-2.0 --> The goal of this content is to set a high-level overview of each tool available. For tools that depend on the backend we should explicitly specify which one we want to use. Backend can also be defined in the configuration file to be used by the user. ## Tools definition In case one wants to add a new sub-command it should decide to in which tool it should be placed. If the new sub-command needs access to the host OS of direct access to a running container then it should be added to MGRADM. Commands in MGRCTL should use the API only. Any command to manage the proxy deployment must be placed in MGRPXY. MGRDEV is focused on utility commands to be used during the development process. ## MGRADM **Goals and definition:** Install, update, and maintain a containerized Uyuni Server. Commands placed here will have/need access to the container runtime environment and also to the HOST OS. Any new command that needs direct access to the host OS or any running container must be added to these tools. **Target Stakeholder:** Uyuni administrator **Where to install:** System where Uyuni Server should be deployed **Sub-commands Naming:** verb -> backend ## MGRCTL **Goals and definition:** Helper tool for day-to-day operations and integration with other tools. Sub-commands in this tool should use the API calls (although case-by-case exceptions can be considered if there are valid reasons). **Target Stakeholder:** Uyuni operators **Where to install:** System where the Uyuni Server is deployed, or in the operator machine (supporting the same Operating Systems we already support for `spacecmd`). **Sub-commands Naming:** subcommand -> verb ## MGRDEV **Goals and definition:** Utility commands to be used during development process. This tool can have commands that run remotely on the host OS or on running containers. These commands can use SSH and podman-socket. Examples of sub-commands are `cp` and `exec`. **Target Stakeholder:** Uyuni Developers **Where to install:** Any machine that needs remote access to running containers. **Sub-commands Naming:** verb -> backend ## MGRPXY **Goals and definition:** Install and manage a containerized Uyuni Proxy. This new command is a proposal to solve the problem of managing the Proxy using the same tool that manages the server, and how that can lead to confusion and errors. **Target Stakeholder:** Uyuni administrator **Where to install:** System where the Uyuni Proxy should be deployed **Sub-commands Naming:** verb -> backend This command is to be developed in a later stage since it would be better to redefine how we deploy containerized proxy and follow the same approach we have provided in the server. 070701000000BB000081B400000000000000000000000165DF4690000001E2000000000000000000000000000000000000001400000000uyuni-tools/push.sh#!/usr/bin/bash # SPDX-FileCopyrightText: 2024 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 # This script is called by push-packages-to-obs OSCAPI=$1 GIT_DIR=$2 PKG_NAME=$3 SRPM_PKG_DIR=$(dirname "$0") if [ "${OSCAPI}" == "https://api.suse.de" ]; then sed 's/^tag=%{!?_default_tag:latest}/tag=5.0.0-beta1/' -i ${SRPM_PKG_DIR}/uyuni-tools.spec sed "s/namespace='%{_default_namespace}'/namespace='%{_default_namespace}\/%{_arch}'/" -i ${SRPM_PKG_DIR}/uyuni-tools.spec fi 070701000000BC000081FD00000000000000000000000165DF4690000000C6000000000000000000000000000000000000001500000000uyuni-tools/setup.sh# SPDX-FileCopyrightText: 2023 SUSE LLC # # SPDX-License-Identifier: Apache-2.0 set -euxo pipefail go mod vendor && tar czvf vendor.tar.gz vendor >/dev/null && rm -rf vendor echo "vendor.tar.gz" 070701000000BD000041FD00000000000000000000000865DF469000000000000000000000000000000000000000000000001300000000uyuni-tools/shared070701000000BE000041FD00000000000000000000000465DF469000000000000000000000000000000000000000000000001700000000uyuni-tools/shared/api070701000000BF000081B400000000000000000000000165DF469000001DB8000000000000000000000000000000000000001E00000000uyuni-tools/shared/api/api.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package api import ( "crypto/tls" "crypto/x509" "os" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/utils" "bytes" "encoding/json" "fmt" "net/http" "time" ) const root_path_apiv1 = "/rhn/manager/api" // HTTP Client is an API entrypoint. type HTTPClient struct { // URL to the API endpoint of the target host BaseURL string // net/http client Client *http.Client // Authentication cookie storage AuthCookie *http.Cookie } // Connection details for initial API connection. type ConnectionDetails struct { // FQDN of the target host. Server string // User to login under. User string // Password for the user. Password string // CA certificate used for target host validation. // Provided certificate is used together with system certificates. CAcert string // Disable certificate validation, unsecure and not recommended. Insecure bool } // API response where T is the type of the result. type ApiResponse[T interface{}] struct { Result T Success bool Message string } // AddAPIFlags is a helper to include api details for the provided command tree. // // If the api support is only optional for the command, set optional parameter to true. func AddAPIFlags(cmd *cobra.Command, optional bool) error { cmd.PersistentFlags().String("api-server", "", "FQDN of the server to connect to") cmd.PersistentFlags().String("api-user", "", "API user username") cmd.PersistentFlags().String("api-password", "", "Password for the API user") cmd.PersistentFlags().String("api-cacert", "", "Path to a cert file of the CA") cmd.PersistentFlags().Bool("api-insecure", false, "If set, server certificate will not be checked for validity") if !optional { if err := cmd.MarkFlagRequired("api-server"); err != nil { return err } if err := cmd.MarkFlagRequired("api-username"); err != nil { return err } if err := cmd.MarkFlagRequired("api-password"); err != nil { return err } } return nil } func prettyPrint(v interface{}) string { b, err := json.MarshalIndent(v, "", " ") if err != nil { return "" } return fmt.Sprintln(string(b)) } func (c *HTTPClient) sendRequest(req *http.Request) (*http.Response, error) { log.Debug().Msgf("Sending %s request %s", req.Method, req.URL) req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Accept", "application/json; charset=utf-8") if c.AuthCookie != nil { req.AddCookie(c.AuthCookie) } log.Trace().Msg(prettyPrint(req.Header)) log.Trace().Msg(prettyPrint(req.Body)) res, err := c.Client.Do(req) if err != nil { return nil, err } log.Trace().Msg(prettyPrint(res.Header)) log.Trace().Msg(prettyPrint(res.Body)) if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { var errResponse map[string]string if err = json.NewDecoder(res.Body).Decode(&errResponse); err == nil { return nil, fmt.Errorf(errResponse["message"]) } return nil, fmt.Errorf("unknown error: %d", res.StatusCode) } log.Debug().Msgf("Received response with code %d", res.StatusCode) return res, nil } // Init returns a HTTPClient object for further API use. // // Provided connectionDetails must have Server specified with FQDN to the // target host. // // Optionaly connectionDetails can have user name and password set and Init // will try to login to the host. // caCert can be set to use custom CA certificate to validate target host. func Init(conn *ConnectionDetails) (*HTTPClient, error) { caCertPool, err := x509.SystemCertPool() if err != nil { log.Warn().Msg(err.Error()) } if conn.CAcert != "" { caCert, err := os.ReadFile(conn.CAcert) if err != nil { log.Fatal().Msg(err.Error()) } caCertPool.AppendCertsFromPEM(caCert) } client := &HTTPClient{ BaseURL: fmt.Sprintf("https://%s%s", conn.Server, root_path_apiv1), Client: &http.Client{ Timeout: time.Minute, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: caCertPool, InsecureSkipVerify: conn.Insecure, }, }, }, } if len(conn.User) > 0 { if len(conn.Password) == 0 { utils.AskPasswordIfMissing(&conn.Password, "API server password") } err = client.login(conn) } return client, err } func (c *HTTPClient) login(conn *ConnectionDetails) error { url := fmt.Sprintf("%s/%s", c.BaseURL, "auth/login") data := map[string]string{ "login": conn.User, "password": conn.Password, } jsonData, err := json.Marshal(data) if err != nil { log.Error().Err(err).Msg("Unable to create login data") return err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return err } res, err := c.sendRequest(req) if err != nil { return err } var response map[string]interface{} if err = json.NewDecoder(res.Body).Decode(&response); err != nil { return err } if !response["success"].(bool) { return fmt.Errorf(response["messages"].(string)) } cookies := res.Cookies() for _, cookie := range cookies { if cookie.Name == "pxt-session-cookie" && cookie.MaxAge > 0 { c.AuthCookie = cookie break } } if c.AuthCookie == nil { return fmt.Errorf("auth cookie not found in login response") } return nil } // Post issues a POST HTTP request to the API target // // `path` specifies an API endpoint // `data` contains a map of values to add to the POST query. `data` are serialized to the JSON // // returns a raw HTTP Response. func (c *HTTPClient) Post(path string, data map[string]interface{}) (*http.Response, error) { url := fmt.Sprintf("%s/%s", c.BaseURL, path) jsonData, err := json.Marshal(data) if err != nil { log.Error().Err(err).Msg("Unable to JSONify data") return nil, err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } res, err := c.sendRequest(req) if err != nil { return nil, err } return res, nil } // Get issues GET HTTP request to the API target // // `path` specifies API endpoint together with query options // // returns a raw HTTP Response. func (c *HTTPClient) Get(path string) (*http.Response, error) { url := fmt.Sprintf("%s/%s", c.BaseURL, path) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } res, err := c.sendRequest(req) if err != nil { return nil, err } return res, nil } // Post issues a POST HTTP request to the API target using the client and decodes the response. // // `path` specifies an API endpoint // `data` contains a map of values to add to the POST query. `data` are serialized to the JSON // // returns a deserialized JSON data to the map. func Post[T interface{}](client *HTTPClient, path string, data map[string]interface{}) (*ApiResponse[T], error) { res, err := client.Post(path, data) if err != nil { return nil, err } defer res.Body.Close() var response ApiResponse[T] if err = json.NewDecoder(res.Body).Decode(&response); err != nil { return nil, err } return &response, nil } // Get issues an HTTP GET request to the API using the client and decodes the response. // // `path` specifies API endpoint together with query options // // returns an ApiResponse with the decoded result. func Get[T interface{}](client *HTTPClient, path string) (*ApiResponse[T], error) { res, err := client.Get(path) if err != nil { return nil, err } defer res.Body.Close() var response ApiResponse[T] if err = json.NewDecoder(res.Body).Decode(&response); err != nil { return nil, err } return &response, nil } 070701000000C0000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001B00000000uyuni-tools/shared/api/org070701000000C1000081B400000000000000000000000165DF469000000480000000000000000000000000000000000000002A00000000uyuni-tools/shared/api/org/createFirst.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package org import ( "errors" "fmt" "github.com/uyuni-project/uyuni-tools/shared/api" "github.com/uyuni-project/uyuni-tools/shared/api/types" ) // Create first organization and user after initial setup without authentication. // orgName is the name of the first organization to create and admin the user to create. func CreateFirst(cnxDetails *api.ConnectionDetails, orgName string, admin *types.User) (*types.Organization, error) { client, err := api.Init(cnxDetails) if err != nil { return nil, fmt.Errorf("failed to connect to the server: %s", err) } data := map[string]interface{}{ "orgName": orgName, "adminLogin": admin.Login, "adminPassword": admin.Password, "firstName": admin.FirstName, "lastName": admin.LastName, "email": admin.Email, } res, err := api.Post[types.Organization](client, "org/createFirst", data) if err != nil { return nil, fmt.Errorf("failed to create first user and organization: %s", err) } if !res.Success { return nil, errors.New(res.Message) } return &res.Result, nil } 070701000000C2000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001D00000000uyuni-tools/shared/api/types070701000000C3000081B400000000000000000000000165DF46900000029E000000000000000000000000000000000000002D00000000uyuni-tools/shared/api/types/organization.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // Organization describe an organization in the API. type Organization struct { Id int Name string ActiveUsers int `mapstructure:"active_users"` Systems int Trusts int SystemGroups int `mapstructure:"system_groups"` ActivationKeys int `mapstructure:"activation_keys"` KickstartProfiles int `mapstructure:"kickstart_profiles"` ConfigurationChannels int `mapstructure:"configuration_channels"` StagingContentEnabled bool `mapstructure:"staging_content_enabled"` } 070701000000C4000081B400000000000000000000000165DF4690000000FE000000000000000000000000000000000000002500000000uyuni-tools/shared/api/types/user.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // User describes an Uyuni user in the API. type User struct { Login string Password string FirstName string LastName string Email string } 070701000000C5000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001E00000000uyuni-tools/shared/completion070701000000C6000081B400000000000000000000000165DF469000000535000000000000000000000000000000000000002C00000000uyuni-tools/shared/completion/completion.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package completion import ( "fmt" "os" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) // NewCommand command for generates completion script. func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command { shellCompletionCmd := &cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate shell completion script", Long: "Generate shell completion script", DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish"}, Args: cobra.ExactValidArgs(1), Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { switch args[0] { case "bash": if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil { return fmt.Errorf("cannot generate bash completion: %s", err) } case "zsh": if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil { return fmt.Errorf("cannot generate zsh completion: %s", err) } case "fish": if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil { return fmt.Errorf("cannot generate fish completion: %s", err) } } return nil }, } return shellCompletionCmd } 070701000000C7000081B400000000000000000000000165DF4690000021B8000000000000000000000000000000000000002100000000uyuni-tools/shared/connection.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package shared import ( "bytes" "errors" "fmt" "os/exec" "strings" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/pflag" "github.com/uyuni-project/uyuni-tools/shared/kubernetes" "github.com/uyuni-project/uyuni-tools/shared/podman" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // Connection contains information about how to connect to the server. type Connection struct { backend string command string podName string podmanContainer string kubernetesFilter string } // Create a new connection object. // The backend is either the command to use to connect to the container or the empty string. // // The empty strings means automatic detection of the backend where the uyuni container is running. // podmanContainer is the name of a podman container to look for when detecting the command. // kubernetesFilter is a filter parameter to use to match a pod. func NewConnection(backend string, podmanContainer string, kubernetesFilter string) *Connection { cnx := Connection{backend: backend, podmanContainer: podmanContainer, kubernetesFilter: kubernetesFilter} return &cnx } // GetCommand validates or guesses the connection backend command. func (c *Connection) GetCommand() (string, error) { var err error if c.command == "" { switch c.backend { case "podman": fallthrough case "podman-remote": fallthrough case "kubectl": if _, err = exec.LookPath(c.backend); err != nil { err = fmt.Errorf("backend command not found in PATH: %s", c.backend) } c.command = c.backend case "": hasPodman := false hasKubectl := false // Check kubectl with a timeout in case the configured cluster is not responding _, err = exec.LookPath("kubectl") if err == nil { hasKubectl = true if out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "--request-timeout=30s", "get", "pod", c.kubernetesFilter, "-A", "-o=jsonpath={.items[*].metadata.name}"); err != nil { log.Info().Msg("kubectl not configured to connect to a cluster, ignoring") } else if len(bytes.TrimSpace(out)) != 0 { c.command = "kubectl" return c.command, err } } // Search for other backends bins := []string{"podman", "podman-remote"} for _, bin := range bins { if _, err = exec.LookPath(bin); err == nil { hasPodman = true if checkErr := utils.RunCmd(bin, "inspect", c.podmanContainer, "--format", "{{.Name}}"); checkErr == nil { c.command = bin break } } } if c.command == "" { // Check for uyuni-server.service or helm release if hasPodman && podman.HasService("uyuni-server") { c.command = "podman" } else if hasKubectl { clusterInfos := kubernetes.CheckCluster() if kubernetes.HasHelmRelease("uyuni", clusterInfos.GetKubeconfig()) { c.command = "kubectl" } } } if c.command == "" { err = fmt.Errorf("uyuni container is not accessible with one of podman, podman-remote or kubectl") } default: err = fmt.Errorf("unsupported backend %s", c.backend) } } return c.command, err } // GetPodName finds the name of the running pod. func (c *Connection) GetPodName() (string, error) { var err error if c.podName == "" { command, cmdErr := c.GetCommand() if cmdErr != nil { log.Fatal().Err(cmdErr) } switch command { case "podman-remote": fallthrough case "podman": if out, _ := utils.RunCmdOutput(zerolog.DebugLevel, c.command, "ps", "-q", "-f", "name="+c.podmanContainer); len(out) == 0 { err = fmt.Errorf("container %s is not running on podman", c.podmanContainer) } else { c.podName = c.podmanContainer } case "kubectl": // We try the first item on purpose to make the command fail if not available podName, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "pod", c.kubernetesFilter, "-A", "-o=jsonpath={.items[0].metadata.name}") if err == nil { c.podName = string(podName[:]) } } } return c.podName, err } // Exec runs command inside the container within an sh shell. func (c *Connection) Exec(command string, args ...string) ([]byte, error) { if c.podName == "" { if _, err := c.GetPodName(); c.podName == "" { return nil, fmt.Errorf("the container is not running, %s %s command not executed: %s", command, strings.Join(args, " "), err) } } cmd, cmdErr := c.GetCommand() if cmdErr != nil { return nil, cmdErr } cmdArgs := []string{"exec", c.podName} if cmd == "kubectl" { cmdArgs = append(cmdArgs, "-c", "uyuni", "--") } shellArgs := append([]string{command}, args...) cmdArgs = append(cmdArgs, shellArgs...) return utils.RunCmdOutput(zerolog.DebugLevel, cmd, cmdArgs...) } // WaitForServer waits at most 60s for multi-user systemd target to be reached. func (c *Connection) WaitForServer() error { // Wait for the system to be up for i := 0; i < 60; i++ { podName, err := c.GetPodName() if err != nil { log.Fatal().Err(err) } args := []string{"exec", podName} command, err := c.GetCommand() if err != nil { log.Fatal().Err(err) } if command == "kubectl" { args = append(args, "--") } args = append(args, "systemctl", "is-active", "-q", "multi-user.target") output := utils.RunCmd(command, args...) isActive := output == nil if isActive { return nil } time.Sleep(1 * time.Second) } return errors.New("server didn't start within 60s. Check for the service status") } // Copy transfers a file to or from the container. // Prefix one of src or dst parameters with `server:` to designate the path is in the container // user and group parameters are used to set the owner of a file transferred in the container. func (c *Connection) Copy(src string, dst string, user string, group string) error { podName, err := c.GetPodName() if err != nil { return err } var commandArgs []string extraArgs := []string{} srcExpanded := strings.Replace(src, "server:", podName+":", 1) dstExpanded := strings.Replace(dst, "server:", podName+":", 1) command, err := c.GetCommand() if err != nil { return err } switch command { case "podman-remote": fallthrough case "podman": commandArgs = []string{"cp", srcExpanded, dstExpanded} case "kubectl": commandArgs = []string{"cp", "-c", "uyuni", srcExpanded, dstExpanded} extraArgs = []string{"-c", "uyuni", "--"} default: return fmt.Errorf("unknown container kind: %s", command) } if err := utils.RunCmdStdMapping(command, commandArgs...); err != nil { return err } if user != "" && strings.HasPrefix(dst, "server:") { execArgs := []string{"exec", podName} execArgs = append(execArgs, extraArgs...) owner := user if group != "" { owner = user + ":" + group } execArgs = append(execArgs, "chown", owner, strings.Replace(dst, "server:", "", 1)) return utils.RunCmdStdMapping(command, execArgs...) } return nil } // TestExistenceInPod returns true if dstpath exists in the pod. func (c *Connection) TestExistenceInPod(dstpath string) bool { podName, err := c.GetPodName() if err != nil { log.Fatal().Err(err) } commandArgs := []string{"exec", podName} command, err := c.GetCommand() if err != nil { log.Fatal().Err(err) } switch command { case "podman": commandArgs = append(commandArgs, "test", "-e", dstpath) case "kubectl": commandArgs = append(commandArgs, "-c", "uyuni", "test", "-e", dstpath) default: log.Fatal().Msgf("Unknown container kind: %s\n", command) } if _, err := utils.RunCmdOutput(zerolog.DebugLevel, command, commandArgs...); err != nil { return false } return true } // ChoosePodmanOrKubernetes selects either the podman or the kubernetes function based on the backend. // This function automatically detects the backend if compiled with kubernetes support and the backend flag is not passed. func ChoosePodmanOrKubernetes[F interface{}]( flags *pflag.FlagSet, podmanFn utils.CommandFunc[F], kubernetesFn utils.CommandFunc[F], ) (utils.CommandFunc[F], error) { backend := "podman" if utils.KubernetesBuilt { backend, _ = flags.GetString("backend") } cnx := NewConnection(backend, podman.ServerContainerName, kubernetes.ServerFilter) command, err := cnx.GetCommand() if err != nil { return nil, fmt.Errorf("failed to determine suitable backend") } switch command { case "podman": return podmanFn, nil case "kubectl": return kubernetesFn, nil } // Should never happen if the commands are the same than those handled in GetCommand() return nil, fmt.Errorf("no supported backend found") } 070701000000C8000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001E00000000uyuni-tools/shared/kubernetes070701000000C9000081B400000000000000000000000165DF469000000E9F000000000000000000000000000000000000002600000000uyuni-tools/shared/kubernetes/helm.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "bytes" "encoding/json" "fmt" "os/exec" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // HelmUpgrade runs the helm upgrade command. // // To perform an installation, set the install parameter to true: helm would get the --install parameter. // If repo is not empty, the --repo parameter will be passed. // If version is not empty, the --version parameter will be passed. func HelmUpgrade(kubeconfig string, namespace string, install bool, repo string, name string, chart string, version string, args ...string) error { helmArgs := []string{ "upgrade", "-n", namespace, "--create-namespace", name, chart, } if kubeconfig != "" { helmArgs = append(helmArgs, "--kubeconfig", kubeconfig) } if repo != "" { helmArgs = append(helmArgs, "--repo", repo) } if version != "" { helmArgs = append(helmArgs, "--version", version) } if install { helmArgs = append(helmArgs, "--install") } helmArgs = append(helmArgs, args...) command := "upgrade" if install { command = "install" } errorMessage := fmt.Sprintf("Failed to %s helm chart %s in namespace %s", command, chart, namespace) if err := utils.RunCmdStdMapping("helm", helmArgs...); err != nil { return fmt.Errorf("%s: %s", errorMessage, err) } return nil } // HelmUninstall runs the helm uninstall command to remove a deployment. func HelmUninstall(kubeconfig string, deployment string, filter string, dryRun bool) string { helmArgs := []string{} if kubeconfig != "" { helmArgs = append(helmArgs, "--kubeconfig", kubeconfig) } jsonpath := fmt.Sprintf("jsonpath={.items[?(@.metadata.name==\"%s\")].metadata.namespace}", deployment) args := []string{"get", "-A", "deploy", "-o", jsonpath} if filter != "" { args = append(args, filter) } out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", args...) if err != nil { log.Info().Err(err).Msgf("Failed to find %s's namespace, skipping removal", deployment) } namespace := string(out) if namespace == "" { log.Debug().Msgf("Pod is not running, trying to find the namespace using the helm release") args = append(helmArgs, "list", "-aA", "-f", deployment, "-o", "json") out, err = utils.RunCmdOutput(zerolog.DebugLevel, "helm", args...) if err != nil { log.Info().Err(err).Msgf("Failed to detect %s's namespace using helm", deployment) } var data []releaseInfo if err = json.Unmarshal(out, &data); err != nil { log.Error().Err(err).Msgf("Helm provided an invalid JSON output") } if len(data) == 1 { namespace = data[0].Namespace } } if namespace != "" { helmArgs = append(helmArgs, "uninstall", "-n", namespace, deployment) if dryRun { log.Info().Msgf("Would run helm %s", strings.Join(helmArgs, " ")) } else { log.Info().Msgf("Uninstalling %s", deployment) message := "Failed to run helm " + strings.Join(helmArgs, " ") err := utils.RunCmd("helm", helmArgs...) if err != nil { log.Fatal().Err(err).Msg(message) } } } return namespace } // HasHelmRelease returns whether a helm release is installed or not, even if it failed. func HasHelmRelease(release string, kubeconfig string) bool { if _, err := exec.LookPath("helm"); err == nil { args := []string{} if kubeconfig != "" { args = append(args, "--kubeconfig", kubeconfig) } args = append(args, "list", "-aAq", "--no-headers", "-f", release) out, err := utils.RunCmdOutput(zerolog.TraceLevel, "helm", args...) return len(bytes.TrimSpace(out)) != 0 && err != nil } return false } type releaseInfo struct { Namespace string `mapstructure:"namespace"` } 070701000000CA000081B400000000000000000000000165DF46900000059F000000000000000000000000000000000000002500000000uyuni-tools/shared/kubernetes/k3s.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) const k3sTraefikConfigPath = "/var/lib/rancher/k3s/server/manifests/k3s-traefik-config.yaml" // InstallK3sTraefikConfig install K3s Traefik configuration. func InstallK3sTraefikConfig(tcpPorts []types.PortMap, udpPorts []types.PortMap) { log.Info().Msg("Installing K3s Traefik configuration") data := K3sTraefikConfigTemplateData{ TcpPorts: tcpPorts, UdpPorts: udpPorts, } if err := utils.WriteTemplateToFile(data, k3sTraefikConfigPath, 0600, false); err != nil { log.Fatal().Err(err).Msgf("Failed to write K3s Traefik configuration") } // Wait for traefik to be back log.Info().Msg("Waiting for Traefik to be reloaded") for i := 0; i < 60; i++ { out, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", "get", "job", "-A", "-o", "jsonpath={.status.completionTime}", "helm-install-traefik") if err == nil { completionTime, err := time.Parse(time.RFC3339, string(out)) if err == nil && time.Since(completionTime).Seconds() < 60 { break } } } } // UninstallK3sTraefikConfig uninstall K3s Traefik configuration. func UninstallK3sTraefikConfig(dryRun bool) { utils.UninstallFile(k3sTraefikConfigPath, dryRun) } 070701000000CB000081B400000000000000000000000165DF46900000046A000000000000000000000000000000000000003400000000uyuni-tools/shared/kubernetes/k3sTraefikTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "io" "text/template" "github.com/uyuni-project/uyuni-tools/shared/types" ) const k3sTraefikConfigTemplate = `apiVersion: helm.cattle.io/v1 kind: HelmChartConfig metadata: name: traefik namespace: kube-system spec: valuesContent: |- ports: {{- range .TcpPorts }} {{ .Name }}: port: {{ .Port }} expose: true exposedPort: {{ .Exposed }} protocol: TCP {{- end }} {{- range .UdpPorts }} {{ .Name }}: port: {{ .Port }} expose: true exposedPort: {{ .Exposed }} protocol: UDP {{- end }} ` // K3sTraefikConfigTemplateData represents information used to create K3s Traefik helm chart. type K3sTraefikConfigTemplateData struct { TcpPorts []types.PortMap UdpPorts []types.PortMap } // Render will create the helm chart configuation for K3sTraefik. func (data K3sTraefikConfigTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("k3sTraefikConfig").Parse(k3sTraefikConfigTemplate)) return t.Execute(wr, data) } 070701000000CC000081B400000000000000000000000165DF469000000C0D000000000000000000000000000000000000002C00000000uyuni-tools/shared/kubernetes/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "fmt" "os" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // ClusterInfos represent cluster information. type ClusterInfos struct { KubeletVersion string Ingress string } // IsK3s is true if it's a K3s Cluster. func (infos ClusterInfos) IsK3s() bool { return strings.Contains(infos.KubeletVersion, "k3s") } // IsRKE2 is true if it's a RKE2 Cluster. func (infos ClusterInfos) IsRke2() bool { return strings.Contains(infos.KubeletVersion, "rke2") } // GetKubeconfig returns the path to the default kubeconfig file or "" if none. func (infos ClusterInfos) GetKubeconfig() string { var kubeconfig string if infos.IsK3s() { // If the user didn't provide a KUBECONFIG value or file, use the k3s default kubeconfigPath := os.ExpandEnv("${HOME}/.kube/config") if os.Getenv("KUBECONFIG") == "" || !utils.FileExists(kubeconfigPath) { kubeconfig = "/etc/rancher/k3s/k3s.yaml" } } // Since even kubectl doesn't work without a trick on rke2, we assume the user has set kubeconfig return kubeconfig } // CheckCluster return cluster information. func CheckCluster() ClusterInfos { // Get the kubelet version hostname, err := os.Hostname() if err != nil { log.Fatal().Err(err).Msgf("Failed to get node hostname") } out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "node", "-o", "jsonpath={.status.nodeInfo.kubeletVersion}", hostname) if err != nil { log.Fatal().Err(err).Msgf("Failed to get kubelet version for node %s", hostname) } var infos ClusterInfos infos.KubeletVersion = string(out) infos.Ingress = guessIngress() return infos } func guessIngress() string { var ingress string // Check for a traefik resource err := utils.RunCmd("kubectl", "explain", "ingressroutetcp") if err == nil { ingress = "traefik" } // Look for a pod running the nginx-ingress-controller: there is no other common way to find out out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "pod", "-A", "-o", "jsonpath={range .items[*]}{.spec.containers[*].args[0]}{.spec.containers[*].command}{end}") if err != nil { log.Fatal().Err(err).Msgf("Failed to get pod commands to look for nginx controller") } const nginxController = "/nginx-ingress-controller" if strings.Contains(string(out), nginxController) { ingress = "nginx" } return ingress } // Restart restarts the pod. func Restart(ServerFilter string) error { if err := Stop(ServerFilter); err != nil { return fmt.Errorf("cannot stop %s: %s", ServerFilter, err) } return Start(ServerFilter) } // Start starts the pod. func Start(ServerFilter string) error { // if something is running, we don't need to set replicas to 1 if _, err := GetNode("uyuni"); err != nil { return ReplicasTo(ServerFilter, 1) } log.Debug().Msgf("Already running") return nil } // Stop stop the pod. func Stop(ServerFilter string) error { return ReplicasTo(ServerFilter, 0) } 070701000000CD000081B400000000000000000000000165DF4690000005B3000000000000000000000000000000000000002600000000uyuni-tools/shared/kubernetes/rke2.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "strconv" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) const rke2NginxConfigPath = "/var/lib/rancher/rke2/server/manifests/rke2-ingress-nginx-config.yaml" // InstallRke2NgixConfig install Rke2 Nginx configuration. func InstallRke2NginxConfig(tcpPorts []types.PortMap, udpPorts []types.PortMap, namespace string) { log.Info().Msg("Installing RKE2 Nginx configuration") data := Rke2NginxConfigTemplateData{ Namespace: namespace, TcpPorts: tcpPorts, UdpPorts: udpPorts, } if err := utils.WriteTemplateToFile(data, rke2NginxConfigPath, 0600, false); err != nil { log.Fatal().Err(err).Msgf("Failed to write Rke2 nginx configuration") } // Wait for the nginx controller to be back log.Info().Msg("Waiting for Nginx controller to be reloaded") for i := 0; i < 60; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", "get", "daemonset", "-A", "-o", "jsonpath={.status.numberReady}", "rke2-ingress-nginx-controller") if err == nil { if count, err := strconv.Atoi(string(out)); err == nil && count > 0 { break } } } } // UninstallRke2NgixConfig uninstall Rke2 Nginx configuration. func UninstallRke2NginxConfig(dryRun bool) { utils.UninstallFile(rke2NginxConfigPath, dryRun) } 070701000000CE000081B400000000000000000000000165DF469000000444000000000000000000000000000000000000003300000000uyuni-tools/shared/kubernetes/rke2NginxTemplate.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "io" "text/template" "github.com/uyuni-project/uyuni-tools/shared/types" ) const rke2NginxConfigTemplate = `apiVersion: helm.cattle.io/v1 kind: HelmChartConfig metadata: name: rke2-ingress-nginx namespace: kube-system spec: valuesContent: |- controller: config: hsts: "false" tcp: {{- range .TcpPorts }} {{ .Exposed }}: "{{ $.Namespace }}/uyuni-tcp:{{ .Port }}" {{- end }} udp: {{- range .UdpPorts }} {{ .Exposed }}: "{{ $.Namespace }}/uyuni-udp:{{ .Port }}" {{- end }} ` // Rke2NginxConfigTemplateData represents information used to create Rke2 Ngix helm chart. type Rke2NginxConfigTemplateData struct { Namespace string TcpPorts []types.PortMap UdpPorts []types.PortMap } // Render will create the helm chart configuation for Rke2 Nginx. func (data Rke2NginxConfigTemplateData) Render(wr io.Writer) error { t := template.Must(template.New("rke2NginxConfig").Parse(rke2NginxConfigTemplate)) return t.Execute(wr, data) } 070701000000CF000081B400000000000000000000000165DF469000000206000000000000000000000000000000000000002B00000000uyuni-tools/shared/kubernetes/uninstall.go// SPDX-FileCopyrightText: 2023 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes // Message appended in the uninstall commands for kubernetes. const UninstallHelp = ` Note that removing the volumes could also be handled automatically depending on the StorageClass used when installed on a kubernetes cluster. For instance on a default K3S install, the local-path-provider storage volumes will be automatically removed when deleting the deployment even if --purge-volumes argument is not used.` 070701000000D0000081B400000000000000000000000165DF469000002CB9000000000000000000000000000000000000002700000000uyuni-tools/shared/kubernetes/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package kubernetes import ( "encoding/json" "errors" "fmt" "strconv" "strings" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/types" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // ServerFilter represents filter used to check server app. const ServerFilter = "-lapp=uyuni" // ServerFilter represents filter used to check proxy app. const ProxyFilter = "-lapp=uyuni-proxy" // waitForDeployment waits at most 60s for a kubernetes deployment to have at least one replica. // See [isDeploymentReady] for more details. func WaitForDeployment(namespace string, name string, appName string) error { // Find the name of a replica pod // Using the app label is a shortcut, not the 100% acurate way to get from deployment to pod podName := "" jsonpath := fmt.Sprintf("jsonpath={.items[?(@.metadata.labels.app==\"%s\")].metadata.name}", appName) cmdArgs := []string{"get", "pod", "-o", jsonpath} cmdArgs = addNamespace(cmdArgs, namespace) for i := 0; i < 60; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...) if err == nil { podName = string(out) break } } // We need to wait for the image to be pulled as this can add quite some time // Setting a timeout on this is very hard since it hightly depends on network speed and image size // List the Pulled events from the pod as we may not see the Pulling if the image was already downloaded err := WaitForPulledImage(namespace, podName) if err != nil { return fmt.Errorf("failed to pulled image: %s", err) } log.Info().Msgf("Waiting for %s deployment to be ready in %s namespace\n", name, namespace) // Wait for a replica to be ready for i := 0; i < 60; i++ { // TODO Look for pod failures if IsDeploymentReady(namespace, name) { return nil } time.Sleep(1 * time.Second) } return fmt.Errorf("failed to find a ready replica for deployment %s in namespace %s after 60s", name, namespace) } // WaitForPulledImage wait that image is pulled. func WaitForPulledImage(namespace string, podName string) error { log.Info().Msgf("Waiting for image of %s pod in %s namespace to be pulled", podName, namespace) pulledArgs := []string{"get", "event", "-o", "jsonpath={.items[?(@.reason==\"Pulled\")].message}", "--field-selector", "involvedObject.name=" + podName} pulledArgs = addNamespace(pulledArgs, namespace) failedArgs := []string{"get", "event", "-o", "jsonpath={range .items[?(@.reason==\"Failed\")]}{.message}{\"\\n\"}{end}", "--field-selector", "involvedObject.name=" + podName} failedArgs = addNamespace(failedArgs, namespace) for { // Look for events indicating an image pull issue out, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", failedArgs...) if err != nil { return fmt.Errorf("failed to get failed events for pod %s", podName) } lines := strings.Split(string(out), "\n") for _, line := range lines { if strings.HasPrefix(line, "Failed to pull image") { return errors.New("failed to pull image") } } // Has the image pull finished? out, err = utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", pulledArgs...) if err != nil { return fmt.Errorf("failed to get events for pod %s", podName) } if len(out) > 0 { break } time.Sleep(1 * time.Second) } return nil } // IsDeploymentReady returns true if a kubernetes deployment has at least one ready replica. // The name can also be a filter parameter like -lapp=uyuni. // An empty namespace means searching through all the namespaces. func IsDeploymentReady(namespace string, name string) bool { jsonpath := fmt.Sprintf("jsonpath={.items[?(@.metadata.name==\"%s\")].status.readyReplicas}", name) args := []string{"get", "-o", jsonpath, "deploy"} args = addNamespace(args, namespace) out, err := utils.RunCmdOutput(zerolog.TraceLevel, "kubectl", args...) // kubectl errors out if the deployment or namespace doesn't exist if err == nil { if replicas, _ := strconv.Atoi(string(out)); replicas > 0 { return true } } return false } // ReplicasTo set the replica for an app to the given value. // Scale the number of replicas of the server. func ReplicasTo(filter string, replica uint) error { args := []string{"scale", "deploy", "uyuni", "--replicas"} log.Debug().Msgf("Setting replicas for pod in %s to %d", filter, replica) args = append(args, fmt.Sprint(replica)) _, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", args...) if err != nil { return fmt.Errorf("cannot run kubectl %s: %s", args, err) } pods, err := getPods(ServerFilter) if err != nil { return fmt.Errorf("cannot get pods for %s: %s", filter, err) } for _, pod := range pods { if len(pod) > 0 { err = waitForReplica(pod, replica) if err != nil { return fmt.Errorf("replica to %d failed: %s", replica, err) } } } log.Debug().Msgf("Replicas for pod in %s are now %d", filter, replica) return err } func isPodRunning(podname string, filter string) (bool, error) { pods, err := getPods(filter) if err != nil { return false, fmt.Errorf("cannot check if pod %s is running in app %s: %s", podname, filter, err) } return utils.Contains(pods, podname), nil } func getPods(filter string) (pods []string, err error) { log.Debug().Msgf("Checking all pods for %s", filter) cmdArgs := []string{"get", "pods", filter, "--output=custom-columns=:.metadata.name", "--no-headers"} out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...) if err != nil { return pods, fmt.Errorf("cannot execute %s: %s", strings.Join(cmdArgs, string(" ")), err) } lines := strings.Split(string(out), "\n") pods = append(pods, lines...) log.Debug().Msgf("Pods in %s are %s", filter, pods) return pods, err } func waitForReplicaZero(podname string) error { waitSeconds := 120 cmdArgs := []string{"get", "pod", podname} for i := 0; i < waitSeconds; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...) /* Assume that if the command return an error at the first iteration, it's because it failed, * next iteration because the pod was actually deleted */ if err != nil && i == 0 { return fmt.Errorf("cannot check for replica zero for %s: %s", podname, err) } outStr := strings.TrimSuffix(string(out), "\n") if len(outStr) == 0 { log.Debug().Msgf("Pod %s has been deleted", podname) return nil } time.Sleep(1 * time.Second) } return fmt.Errorf("cannot set replica for %s to zero", podname) } func waitForReplica(podname string, replica uint) error { waitSeconds := 120 log.Debug().Msgf("Checking replica for %s ready to %d", podname, replica) if replica == 0 { return waitForReplicaZero(podname) } cmdArgs := []string{"get", "pod", podname, "--output=custom-columns=STATUS:.status.phase", "--no-headers"} var err error for i := 0; i < waitSeconds; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...) outStr := strings.TrimSuffix(string(out), "\n") if err != nil { return fmt.Errorf("cannot execute %s: %s", strings.Join(cmdArgs, string(" ")), err) } if string(outStr) == "Running" { log.Debug().Msgf("%s pod replica is now %d", podname, replica) break } log.Debug().Msgf("Pod %s replica is %s in %d seconds.", podname, string(out), i) time.Sleep(1 * time.Second) } if err != nil { return fmt.Errorf("pod %s replica is not %d in %s seconds: %s", podname, replica, strconv.Itoa(waitSeconds), err) } return nil } func addNamespace(args []string, namespace string) []string { if namespace != "" { args = append(args, "-n", namespace) } else { args = append(args, "-A") } return args } // GetPullPolicy return pullpolicy in lower case, if exists. func GetPullPolicy(name string) string { policies := map[string]string{ "always": "Always", "never": "Never", "ifnotpresent": "IfNotPresent", } policy := policies[strings.ToLower(name)] if policy == "" { log.Fatal().Msgf("%s is not a valid image pull policy value", name) } return policy } // RunPod runs a pod, waiting for its execution and deleting it. func RunPod(podname string, filter string, image string, pullPolicy string, command string, override ...string) error { arguments := []string{"run", podname, "--image", image, "--image-pull-policy", pullPolicy, filter} if len(override) > 0 { arguments = append(arguments, `--override-type=strategic`) for _, arg := range override { overrideParam := "--overrides=" + arg arguments = append(arguments, overrideParam) } } arguments = append(arguments, "--command", "--", command) err := utils.RunCmdStdMapping("kubectl", arguments...) if err != nil { return fmt.Errorf("cannot run %s using image %s: %s", command, image, err) } err = waitForPod(podname) if err != nil { return fmt.Errorf("deleting pod %s. Status fails with error %s", podname, err) } defer func() { err = DeletePod(podname, filter) }() return nil } // Delete a kubernetes pod named podname. func DeletePod(podname string, filter string) error { isRunning, err := isPodRunning(podname, filter) if err != nil { return fmt.Errorf("cannot delete pod %s: %s", podname, err) } if !isRunning { log.Debug().Msgf("no need to delete pod %s because is not running", podname) return nil } arguments := []string{"delete", "pod", podname} _, err = utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", arguments...) if err != nil { return fmt.Errorf("cannot delete pod %s: %s", podname, err) } return nil } func waitForPod(podname string) error { status := "Succeeded" waitSeconds := 120 log.Debug().Msgf("Checking status for %s pod. Waiting %s seconds until status is %s", podname, strconv.Itoa(waitSeconds), status) cmdArgs := []string{"get", "pod", podname, "--output=custom-columns=STATUS:.status.phase", "--no-headers"} var err error for i := 0; i < waitSeconds; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...) outStr := strings.TrimSuffix(string(out), "\n") if err != nil { return fmt.Errorf("cannot execute %s: %s", strings.Join(cmdArgs, string(" ")), err) } if strings.EqualFold(outStr, status) { log.Debug().Msgf("%s pod status is %s", podname, status) return nil } log.Debug().Msgf("Pod %s status is %s for %d seconds.", podname, outStr, i) time.Sleep(1 * time.Second) } return fmt.Errorf("pod %s status is not %s in %s seconds: %s", podname, status, strconv.Itoa(waitSeconds), err) } // GetNode return the node where the app is running. func GetNode(appName string) (string, error) { nodeName := "" cmdArgs := []string{"get", "pod", "-lapp=" + appName, "-o", "jsonpath={.items[*].spec.nodeName}"} for i := 0; i < 60; i++ { out, err := utils.RunCmdOutput(zerolog.DebugLevel, "kubectl", cmdArgs...) if err == nil { nodeName = string(out) break } } if len(nodeName) > 0 { log.Debug().Msgf("Node name for app %s is: %s", appName, nodeName) } else { return "", fmt.Errorf("cannot find Node name for app %s", appName) } return nodeName, nil } // GenerateOverrideDeployment generate a JSON files represents the deployment information. func GenerateOverrideDeployment(deployData types.Deployment) (string, error) { ret, err := json.Marshal(deployData) if err != nil { return "", fmt.Errorf("cannot marshal deployment %s", err) } return string(ret), nil } 070701000000D1000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001A00000000uyuni-tools/shared/podman070701000000D2000081B400000000000000000000000165DF469000000704000000000000000000000000000000000000002400000000uyuni-tools/shared/podman/images.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "bytes" "fmt" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // Ensure the container image is pulled or pull it if the pull policy allows it. func PrepareImage(image string, pullPolicy string) error { log.Info().Msgf("Ensure image %s is available", image) needsPull, err := checkImage(image, pullPolicy) if err != nil { return err } if needsPull { return pullImage(image) } return nil } func checkImage(image string, pullPolicy string) (bool, error) { if strings.ToLower(pullPolicy) == "always" { return true, nil } out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "images", "--quiet", image) if err != nil { return false, fmt.Errorf("failed to check if image %s has already been pulled", image) } if len(bytes.TrimSpace(out)) == 0 { if pullPolicy == "Never" { return false, fmt.Errorf("image %s is not available and cannot be pulled due to policy", image) } return true, nil } return false, nil } func pullImage(image string) error { log.Info().Msgf("Running podman pull %s", image) return utils.RunCmdStdMapping("podman", "pull", image) } // ShowAvailableTag returns the list of avaialable tag for a given image. func ShowAvailableTag(image string) ([]string, error) { log.Info().Msgf("Running podman image search --list-tags %s --format='{{.Tag}}'", image) out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "image", "search", "--list-tags", image, "--format='{{.Tag}}'") if err != nil { return []string{}, fmt.Errorf("cannot find any tag for image %s: %s", image, err) } tags := strings.Split(string(out), "\n") return tags, nil } 070701000000D3000081B400000000000000000000000165DF469000000E64000000000000000000000000000000000000002500000000uyuni-tools/shared/podman/network.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os/exec" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // The name of the podman network for Uyuni and its proxies. const UyuniNetwork = "uyuni" // SetupNetwork creates the podman network. func SetupNetwork() error { log.Info().Msgf("Setting up %s network", UyuniNetwork) ipv6Enabled := isIpv6Enabled() networkExists, err := IsNetworkPresent(UyuniNetwork) if networkExists && err != nil { return fmt.Errorf("cannot check if network %s is already present", UyuniNetwork) } // check if network exists before trying to get the IPV6 information if networkExists { log.Debug().Msgf("%s network already present", UyuniNetwork) // Check if the uyuni network exists and is IPv6 enabled hasIpv6, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "network", "inspect", "--format", "{{.IPv6Enabled}}", UyuniNetwork) if err == nil { if string(hasIpv6) != "true" && ipv6Enabled { log.Info().Msgf("%s network doesn't have IPv6, deleting existing network to enable IPv6 on it", UyuniNetwork) err := utils.RunCmd("podman", "network", "rm", UyuniNetwork, "--log-level", log.Logger.GetLevel().String()) if err != nil { return fmt.Errorf("failed to remove %s podman network: %s", UyuniNetwork, err) } } else { log.Info().Msgf("Reusing existing %s network", UyuniNetwork) return nil } } } args := []string{"network", "create"} if ipv6Enabled { // An IPv6 network on a host where IPv6 is disabled doesn't work: don't try it. // Check if the networkd backend is netavark out, err := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "info", "--format", "{{.Host.NetworkBackend}}") backend := strings.Trim(string(out), "\n") if err != nil { return fmt.Errorf("failed to find podman's network backend: %s", err) } else if backend != "netavark" { log.Info().Msgf("Podman's network backend (%s) is not netavark, skipping IPv6 enabling on %s network", backend, UyuniNetwork) } else { args = append(args, "--ipv6") } } args = append(args, UyuniNetwork) err = utils.RunCmd("podman", args...) if err != nil { return fmt.Errorf("failed to create %s network with IPv6 enabled: %s", UyuniNetwork, err) } return nil } func isIpv6Enabled() bool { files := []string{ "/sys/module/ipv6/parameters/disable", "/proc/sys/net/ipv6/conf/default/disable_ipv6", "/proc/sys/net/ipv6/conf/all/disable_ipv6", } for _, file := range files { // Mind that we are checking disable files, the semantic is inverted if utils.GetFileBoolean(file) { return false } } return true } // DeleteNetwork deletes the uyuni podman network. // If dryRun is set to true, nothing will be done, only messages logged to explain what would happen. func DeleteNetwork(dryRun bool) { err := utils.RunCmd("podman", "network", "exists", UyuniNetwork) if err != nil { log.Info().Msgf("Network %s already removed", UyuniNetwork) } else { if dryRun { log.Info().Msgf("Would run podman network rm %s", UyuniNetwork) } else { err := utils.RunCmd("podman", "network", "rm", UyuniNetwork) if err != nil { log.Error().Msgf("Failed to remove network %s", UyuniNetwork) } else { log.Info().Msg("Network removed") } } } } // IsNetworkPresent returns whether a network is already present. func IsNetworkPresent(network string) (bool, error) { cmd := exec.Command("podman", "network", "exists", network) if err := cmd.Run(); err != nil { return false, err } return cmd.ProcessState.ExitCode() == 0, nil } 070701000000D4000081B400000000000000000000000165DF4690000011FC000000000000000000000000000000000000002500000000uyuni-tools/shared/podman/systemd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os" "os/exec" "path" "github.com/rs/zerolog/log" "github.com/uyuni-project/uyuni-tools/shared/utils" ) const servicesPath = "/etc/systemd/system/" // Name of the systemd service for the server. const ServerService = "uyuni-server" // Name of the systemd service for the proxy. const ProxyService = "uyuni-proxy-pod" // HasService returns if a systemd service is installed. // name is the name of the service without the '.service' part. func HasService(name string) bool { err := utils.RunCmd("systemctl", "list-unit-files", name+".service") return err != nil } // GetServicePath return the path for a given service. func GetServicePath(name string) string { return path.Join(servicesPath, name+".service") } // UninstallService stops and remove a systemd service. // If dryRun is set to true, nothing happens but messages are logged to explain what would be done. func UninstallService(name string, dryRun bool) { servicePath := GetServicePath(name) if HasService(name) { log.Info().Msgf("Systemd has no %s.service unit", name) } else { if dryRun { log.Info().Msgf("Would run systemctl disable --now %s", name) log.Info().Msgf("Would remove %s", servicePath) } else { log.Info().Msgf("Disable %s service", name) // disable server err := utils.RunCmd("systemctl", "disable", "--now", name) if err != nil { log.Error().Err(err).Msgf("Failed to disable %s service", name) } // Remove the service unit log.Info().Msgf("Remove %s", servicePath) if err := os.Remove(servicePath); err != nil { log.Error().Err(err).Msgf("Failed to remove %s.service file", name) } } } } // ReloadDaemon resets the failed state of services and reload the systemd daemon. // If dryRun is set to true, nothing happens but messages are logged to explain what would be done. func ReloadDaemon(dryRun bool) error { if dryRun { log.Info().Msg("Would run systemctl reset-failed") log.Info().Msg("Would run systemctl daemon-reload") } else { err := utils.RunCmd("systemctl", "reset-failed") if err != nil { return fmt.Errorf("failed to reset-failed systemd") } err = utils.RunCmd("systemctl", "daemon-reload") if err != nil { return fmt.Errorf("failed to reload systemd daemon") } } return nil } // IsServiceRunning returns whether the systemd service is started or not. func IsServiceRunning(service string) (bool, error) { // RestartService restarts the systemd service. cmd := exec.Command("systemctl", "is-active", "-q", service) if err := cmd.Run(); err != nil { return false, err } return cmd.ProcessState.ExitCode() == 0, nil } // RestartService restarts the systemd service. func RestartService(service string) error { if err := utils.RunCmd("systemctl", "restart", service); err != nil { return fmt.Errorf("failed to restart systemd %s.service: %s", service, err) } return nil } // StartService starts the systemd service. func StartService(service string) error { if err := utils.RunCmd("systemctl", "start", service); err != nil { return fmt.Errorf("failed to start systemd %s.service: %s", service, err) } return nil } // StopService starts the systemd service. func StopService(service string) error { if err := utils.RunCmd("systemctl", "stop", service); err != nil { return fmt.Errorf("failed to stop systemd %s.service: %s", service, err) } return nil } // EnableService enables and starts a systemd service. func EnableService(service string) error { if err := utils.RunCmd("systemctl", "enable", "--now", service); err != nil { return fmt.Errorf("failed to enable %s systemd service: %s", service, err) } return nil } // Create new systemd service configuration file. func GenerateSystemdConfFile(serviceName string, section string, body string) error { systemdFilePath := GetServicePath(serviceName) log.Info().Msgf("systemdFilePath: %s", systemdFilePath) systemdConfFolder := systemdFilePath + ".d" log.Info().Msgf("systemdConfFolder: %s", systemdConfFolder) if err := os.MkdirAll(systemdConfFolder, 0750); err != nil { return fmt.Errorf("failed to create %s folder: %s", systemdConfFolder, err) } systemdConfFilePath := path.Join(systemdConfFolder, section+".conf") log.Info().Msgf("systemdConfFilePath: %s", systemdConfFilePath) content := []byte("[" + section + "]" + "\n" + body + "\n") if err := os.WriteFile(systemdConfFilePath, content, 0644); err != nil { return fmt.Errorf("cannot write %s file: %s", systemdConfFilePath, err) } return nil } 070701000000D5000081B400000000000000000000000165DF469000000BCE000000000000000000000000000000000000002300000000uyuni-tools/shared/podman/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package podman import ( "fmt" "os/exec" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/utils" ) // ServerContainerName represents the server container name. const ServerContainerName = "uyuni-server" // ProxyContainerNames represents all the proxy container names. var ProxyContainerNames = []string{ "uyuni-proxy-httpd", "uyuni-proxy-salt-broker", "uyuni-proxy-squid", "uyuni-proxy-ssh", "uyuni-proxy-tftpd", } // PodmanFlags stores the podman arguments. type PodmanFlags struct { Args []string `mapstructure:"arg"` } // AddPodmanInstallFlag add the podman arguments to a command. func AddPodmanInstallFlag(cmd *cobra.Command) { cmd.Flags().StringSlice("podman-arg", []string{}, "Extra arguments to pass to podman") } // EnablePodmanSocket enables the podman socket. func EnablePodmanSocket() error { err := utils.RunCmd("systemctl", "enable", "--now", "podman.socket") if err != nil { return fmt.Errorf("failed to enable podman.socket unit: %s", err) } return err } // DeleteContainer deletes a container based on its name. // If dryRun is set to true, nothing will be done, only messages logged to explain what would happen. func DeleteContainer(name string, dryRun bool) { if out, _ := utils.RunCmdOutput(zerolog.DebugLevel, "podman", "ps", "-a", "-q", "-f", "name="+name); len(out) > 0 { if dryRun { log.Info().Msgf("Would run podman kill %s for container id: %s", name, out) log.Info().Msgf("Would run podman remove %s for container id: %s", name, out) } else { log.Info().Msgf("Run podman kill %s for container id: %s", name, out) err := utils.RunCmd("podman", "kill", name) if err != nil { log.Error().Err(err).Msg("Failed to kill the server") log.Info().Msgf("Run podman remove %s for container id: %s", name, out) err = utils.RunCmd("podman", "rm", name) if err != nil { log.Error().Err(err).Msg("Error removing container") } } } } else { log.Info().Msg("Container already removed") } } // DeleteVolume deletes a podman volume based on its name. // If dryRun is set to true, nothing will be done, only messages logged to explain what would happen. func DeleteVolume(name string, dryRun bool) error { exists, err := isVolumePresent(name) if exists && err != nil { return fmt.Errorf("cannot check if volume %s already exists", name) } if exists { if dryRun { log.Info().Msgf("Would run podman volume rm %s", name) } else { log.Info().Msgf("Run podman volume rm %s", name) err := utils.RunCmd("podman", "volume", "rm", name) if err != nil { log.Error().Err(err).Msgf("Failed to remove volume %s", name) } } } return nil } func isVolumePresent(volume string) (bool, error) { cmd := exec.Command("podman", "volume", "exists", volume) if err := cmd.Run(); err != nil { return false, err } return cmd.ProcessState.ExitCode() == 0, nil } 070701000000D6000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001900000000uyuni-tools/shared/types070701000000D7000081B400000000000000000000000165DF4690000000FD000000000000000000000000000000000000002200000000uyuni-tools/shared/types/chart.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // ChartFlags represents the flags required by charts. type ChartFlags struct { Namespace string Chart string Version string Values string } 070701000000D8000081B400000000000000000000000165DF4690000007C5000000000000000000000000000000000000002700000000uyuni-tools/shared/types/deployment.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // VolumeMount type used for mapping pod definition structure. type VolumeMount struct { MountPath string `json:"mountPath,omitempty"` Name string `json:"name,omitempty"` } // Container type used for mapping pod definition structure. type Container struct { Name string `json:"name,omitempty"` Image string `json:"image,omitempty"` VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` } // PersistentVolumeClaim type used for mapping Volume structure. type PersistentVolumeClaim struct { ClaimName string `json:"claimName,omitempty"` } // HostPath type used for mapping Volume structure. type HostPath struct { Path string `json:"path,omitempty"` Type string `json:"type,omitempty"` } // Secret Item for mapping Secret structure. type SecretItem struct { Key string `json:"key,omitempty"` Path string `json:"path,omitempty"` } // Secret type for mapping Volume structure. type Secret struct { SecretName string `json:"secretName,omitempty"` Items []SecretItem `json:"items,omitempty"` } // Volume type for mapping Spec structure. type Volume struct { Name string `json:"name,omitempty"` PersistentVolumeClaim *PersistentVolumeClaim `json:"persistentVolumeClaim,omitempty"` HostPath *HostPath `json:"hostPath,omitempty"` Secret *Secret `json:"secret,omitempty"` } // Spec type for mapping Deployment structure. type Spec struct { NodeName string `json:"nodeName,omitempty"` RestartPolicy string `json:"restartPolicy,omitempty"` Containers []Container `json:"containers,omitempty"` Volumes []Volume `json:"volumes,omitempty"` } // Deployment type can store k8s deployment data. type Deployment struct { APIVersion string `json:"apiVersion,omitempty"` Spec *Spec `json:"spec,omitempty"` } 070701000000D9000081B400000000000000000000000165DF469000000126000000000000000000000000000000000000002300000000uyuni-tools/shared/types/distro.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // Distribution contains information about the distribution. type Distribution struct { TreeLabel string BasePath string ChannelLabel string InstallType string Arch string } 070701000000DA000081B400000000000000000000000165DF4690000000DF000000000000000000000000000000000000002300000000uyuni-tools/shared/types/global.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // GlobalFlags represents the flags used by all commands. type GlobalFlags struct { ConfigPath string LogLevel string } 070701000000DB000081B400000000000000000000000165DF469000000134000000000000000000000000000000000000002300000000uyuni-tools/shared/types/images.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // ImageFlags represents the flags used by an image. type ImageFlags struct { Name string `mapstructure:"image"` Tag string `mapstructure:"tag"` PullPolicy string `mapstructure:"pullPolicy"` } 070701000000DC000081B400000000000000000000000165DF469000000290000000000000000000000000000000000000002400000000uyuni-tools/shared/types/inspect.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types /* InspectData represents CLI command to run in the container * and the variable where the output is stored. */ type InspectData struct { Variable string CLI string } /* InspectFile represent where the inspect file should be stored * and the command to run in the container. */ type InspectFile struct { Directory string Basename string Commands []InspectData } // NewInspectData creates an InspectData instance. func NewInspectData(variable string, cli string) InspectData { return InspectData{ Variable: variable, CLI: cli, } } 070701000000DD000081B400000000000000000000000165DF4690000000D6000000000000000000000000000000000000002500000000uyuni-tools/shared/types/networks.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package types // PortMap describes a port. type PortMap struct { Name string Exposed int Port int Protocol string } 070701000000DE000041FD00000000000000000000000265DF469000000000000000000000000000000000000000000000001900000000uyuni-tools/shared/utils070701000000DF000081B400000000000000000000000165DF469000000864000000000000000000000000000000000000002000000000uyuni-tools/shared/utils/cmd.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "fmt" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/uyuni-project/uyuni-tools/shared/types" ) // DefaultNamespace represents the default name used for image. var DefaultNamespace = "registry.opensuse.org/uyuni" // DefaultTag represents the default tag used for image. var DefaultTag = "latest" // This variable needs to be set a build time using git tags. var Version = "0.0.0" // CommandFunc is a function to be executed by a Cobra command. type CommandFunc[F interface{}] func(*types.GlobalFlags, *F, *cobra.Command, []string) error // CommandHelper parses the configuration file into the flags and runs the fn function. // This function should be passed to Command's RunE. func CommandHelper[T interface{}]( globalFlags *types.GlobalFlags, cmd *cobra.Command, args []string, flags *T, fn CommandFunc[T], ) error { viper, err := ReadConfig(globalFlags.ConfigPath, cmd) if err != nil { return err } if err := viper.Unmarshal(&flags); err != nil { log.Error().Err(err).Msgf("Failed to unmarshall configuration") return fmt.Errorf("failed to unmarshall configuration: %s", err) } return fn(globalFlags, flags, cmd, args) } // AddBackendFlag add the flag for setting the backend ('podman', 'podman-remote', 'kubectl'). func AddBackendFlag(cmd *cobra.Command) { cmd.Flags().String("backend", "", "tool to use to reach the container. Possible values: 'podman', 'podman-remote', 'kubectl'. Default guesses which to use.") } // AddPullPolicyFlag adds the --pullPolicy flag to a command. // // Since podman doesn't have such a concept of pull policy like kubernetes, // the values need some explanations for it: // - Never: just check and fail if needed // - IfNotPresent: check and pull // - Always: pull without checking // // For kubernetes the value is simply passed to the helm charts. func AddPullPolicyFlag(cmd *cobra.Command) { cmd.Flags().String("pullPolicy", "IfNotPresent", "set whether to pull the images or not. The value can be one of 'Never', 'IfNotPresent' or 'Always'") } 070701000000E0000081B400000000000000000000000165DF469000000ED5000000000000000000000000000000000000002300000000uyuni-tools/shared/utils/config.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "fmt" "os" "path" "strings" "text/template" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" ) const envPrefix = "UYUNI" const appName = "uyuni-tools" const configFilename = "config.yaml" // ReadConfig parse configuration file and env variables a return parameters. func ReadConfig(configPath string, cmd *cobra.Command) (*viper.Viper, error) { v := viper.New() v.SetConfigType("yaml") v.SetConfigName(configFilename) if configPath != "" { log.Info().Msgf("Using config file %s", configPath) v.SetConfigFile(configPath) } else { xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") if xdgConfigHome == "" { home, err := os.UserHomeDir() if err != nil { log.Err(err).Msg("Failed to find home directory") } else { xdgConfigHome = path.Join(home, ".config") } } if xdgConfigHome != "" { v.AddConfigPath(path.Join(xdgConfigHome, appName)) } v.AddConfigPath(".") } if err := bindFlags(cmd, v); err != nil { return nil, err } if err := v.ReadInConfig(); err != nil { // It's okay if there isn't a config file if _, ok := err.(viper.ConfigFileNotFoundError); !ok { // TODO Provide help on the config file format return nil, fmt.Errorf("failed to parse configuration file %s: %s", v.ConfigFileUsed(), err) } } v.SetEnvPrefix(envPrefix) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() return v, nil } // Bind each cobra flag to its associated viper configuration (config file and environment variable). func bindFlags(cmd *cobra.Command, v *viper.Viper) error { var errors []error cmd.Flags().VisitAll(func(f *pflag.Flag) { configName := strings.ReplaceAll(f.Name, "-", ".") if err := v.BindPFlag(configName, f); err != nil { errors = append(errors, fmt.Errorf("failed to bind %s config to parameter %s: %s", configName, f.Name, err)) } }) if len(errors) > 0 { return errors[0] } return nil } const configTemplate = ` Configuration: All the non-global flags can alternatively be passed as configuration. The configuration file is a YAML file with entries matching the flag name. The name of a flag is the part after the '--' of the command line parameter. Every '_' character in the flag name means a nested property. For instance the '--tz CEST' and '--ssl-password secret' will be mapped to this YAML configuration: tz: CEST ssl: password: secret The configuration file will be searched in the following places and order: · $XDG_CONFIG_HOME/{{ .Name }}/{{ .ConfigFile }} · $HOME/.config/{{ .Name }}/{{ .ConfigFile }} · $PWD/{{ .ConfigFile }} · the value of the --config flag Environment variables: All the non-global flags can also be passed as environment variables. The environment variable name is the flag name with '-' replaced by with '_' and the {{ .EnvPrefix }} prefix. For example the '--tz CEST' flag will be mapped to '{{ .EnvPrefix }}_TZ' and '--ssl-password' flags to '{{ .EnvPrefix }}_SSL_PASSWORD' ` // GetUsageWithConfigHelpTemplate returns the usage template with the configuration help added. func GetUsageWithConfigHelpTemplate(usageTemplate string) (string, error) { t := template.Must(template.New("help").Parse(configTemplate)) var helpBuilder strings.Builder if err := t.Execute(&helpBuilder, configTemplateData{ EnvPrefix: envPrefix, Name: appName, ConfigFile: configFilename, }); err != nil { return "", fmt.Errorf("cannot return usage template: %s", err) } return usageTemplate + helpBuilder.String(), nil } type configTemplateData struct { EnvPrefix string ConfigFile string Name string } 070701000000E1000081B400000000000000000000000165DF469000000684000000000000000000000000000000000000002100000000uyuni-tools/shared/utils/exec.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "os" "os/exec" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) // OutputLogWriter contains information output the logger and the loglevel. type OutputLogWriter struct { Logger zerolog.Logger LogLevel zerolog.Level } // Write writes a byte array to an OutputLogWriter. func (l OutputLogWriter) Write(p []byte) (n int, err error) { n = len(p) if n > 0 && p[n-1] == '\n' { // Trim CR added by stdlog. p = p[0 : n-1] } l.Logger.WithLevel(l.LogLevel).CallerSkipFrame(1).Msg(string(p)) return } // RunCmd execute a shell command. func RunCmd(command string, args ...string) error { log.Debug().Msgf("Running: %s %s", command, strings.Join(args, " ")) return exec.Command(command, args...).Run() } // RunCmdStdMapping execute a shell command mapping the stdout and stderr. func RunCmdStdMapping(command string, args ...string) error { log.Debug().Msgf("Running: %s %s", command, strings.Join(args, " ")) runCmd := exec.Command(command, args...) runCmd.Stdout = os.Stdout runCmd.Stderr = os.Stderr return runCmd.Run() } // RunCmdOutput execute a shell command and collects output. func RunCmdOutput(logLevel zerolog.Level, command string, args ...string) ([]byte, error) { log.Debug().Msgf("Running: %s %s", command, strings.Join(args, " ")) output, err := exec.Command(command, args...).Output() if err != nil { log.Warn().Msgf("Command returned with error: %s. Be sure to handle the error if required", err) } else if len(output) > 0 { log.Debug().Msgf("Command output: %s", output) } return output, err } 070701000000E2000081B400000000000000000000000165DF469000000103000000000000000000000000000000000000002700000000uyuni-tools/shared/utils/kubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build !nok8s package utils // KubernetesBuilt is a flag for compiling kubernes code. True when go:build !nok8s, False when go:build nok8s. const KubernetesBuilt = true 070701000000E3000081B400000000000000000000000165DF469000000803000000000000000000000000000000000000002500000000uyuni-tools/shared/utils/logUtils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "io" "os" "path" "strconv" "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "gopkg.in/natefinch/lumberjack.v2" ) // LogInit initialize logs. func LogInit(logToConsole bool) { zerolog.CallerMarshalFunc = logCallerMarshalFunction zerolog.SetGlobalLevel(zerolog.InfoLevel) fileWriter := getFileWriter() writers := []io.Writer{fileWriter} if logToConsole { writers = append(writers, zerolog.NewConsoleWriter()) } multi := zerolog.MultiLevelWriter(writers...) log.Logger = zerolog.New(multi).With().Timestamp().Stack().Logger() } func getFileWriter() *lumberjack.Logger { const globalLogPath = "/var/log/" logPath := globalLogPath if file, err := os.OpenFile(globalLogPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600); err != nil { logPath, err = os.UserHomeDir() if err != nil { logPath = "./" } } else { file.Close() } fileLogger := &lumberjack.Logger{ Filename: path.Join(logPath, "uyuni-tools.log"), MaxSize: 5, MaxBackups: 5, MaxAge: 90, Compress: true, } return fileLogger } // SetLogLevel sets the loglevel. func SetLogLevel(logLevel string) { globalLevel := zerolog.InfoLevel level, err := zerolog.ParseLevel(logLevel) if logLevel != "" && err == nil { globalLevel = level } if globalLevel <= zerolog.DebugLevel { log.Logger = log.Logger.With().Caller().Logger() } zerolog.SetGlobalLevel(globalLevel) } func logCallerMarshalFunction(pc uintptr, file string, line int) string { paths := strings.Split(file, "/") callerFile := file foundSubDir := false if strings.HasSuffix(file, "/io/io.go") { return "Cmd output" } for _, currentPath := range paths { if foundSubDir { if callerFile != "" { callerFile = callerFile + "/" } callerFile = callerFile + currentPath } else { if strings.Contains(currentPath, "uyuni-tools") { foundSubDir = true callerFile = "" } } } return callerFile + ":" + strconv.Itoa(line) } 070701000000E4000081B400000000000000000000000165DF469000000093000000000000000000000000000000000000002900000000uyuni-tools/shared/utils/nokubernetes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 //go:build nok8s package utils const KubernetesBuilt = false 070701000000E5000081B400000000000000000000000165DF46900000068B000000000000000000000000000000000000002200000000uyuni-tools/shared/utils/ports.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import "github.com/uyuni-project/uyuni-tools/shared/types" // NewPortMap is a constructor for PortMap type. func NewPortMap(name string, exposed int, port int) types.PortMap { return types.PortMap{ Name: name, Exposed: exposed, Port: port, } } // TCP_PORTS are the tcp ports required by the server // The port names should be less than 15 characters long and lowercased for traefik to eat them. var TCP_PORTS = []types.PortMap{ NewPortMap("postgres", 5432, 5432), NewPortMap("salt-publish", 4505, 4505), NewPortMap("salt-request", 4506, 4506), NewPortMap("cobbler", 25151, 25151), NewPortMap("psql-mtrx", 9187, 9187), NewPortMap("tasko-jmx-mtrx", 5556, 5556), NewPortMap("tomcat-jmx-mtrx", 5557, 5557), } // DEBUG_PORTS are the port used by dev for debugging applications. var DEBUG_PORTS = []types.PortMap{ // We can't expose on port 8000 since traefik already uses it NewPortMap("tomcat-debug", 8003, 8003), NewPortMap("tasko-debug", 8001, 8001), NewPortMap("search-debug", 8002, 8002), } // UDP_PORTS are the udp ports required by the server. var UDP_PORTS = []types.PortMap{ { Name: "tftp", Exposed: 69, Port: 69, Protocol: "udp", }, } // PROXY_TCP_PORTS are the tcp ports required by the proxy. var PROXY_TCP_PORTS = []types.PortMap{ NewPortMap("ssh", 8022, 22), NewPortMap("salt-publish", 4505, 4505), NewPortMap("salt-request", 4506, 4506), } // PROXY_PODMAN_PORTS are the http/s ports required by the proxy. var PROXY_PODMAN_PORTS = []types.PortMap{ NewPortMap("https", 443, 443), NewPortMap("http", 80, 8080), } 070701000000E6000081B400000000000000000000000165DF469000000136000000000000000000000000000000000000002300000000uyuni-tools/shared/utils/slices.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils // Contains returns true if a string is contained in a string slice. func Contains(slice []string, needle string) bool { for _, item := range slice { if item == needle { return true } } return false } 070701000000E7000081B400000000000000000000000165DF469000000A6E000000000000000000000000000000000000002000000000uyuni-tools/shared/utils/tar.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "path/filepath" "strings" "github.com/rs/zerolog/log" ) // Extracts a tar.gz file. func ExtractTarGz(tarballPath string, dstPath string) error { reader, err := os.Open(tarballPath) if err != nil { return err } defer reader.Close() archive, err := gzip.NewReader(reader) if err != nil { return err } defer archive.Close() tarReader := tar.NewReader(archive) for { header, err := tarReader.Next() if err == io.EOF { break } else if err != nil { return err } path, err := filepath.Abs(filepath.Join(dstPath, header.Name)) if err != nil { return err } if !strings.HasPrefix(path, dstPath) { log.Warn().Msgf("Skipping extraction of %s in %s file as is resolves outside the target path", header.Name, tarballPath) continue } info := header.FileInfo() if info.IsDir() { log.Debug().Msgf("Creating folder %s", path) if err = os.MkdirAll(path, info.Mode()); err != nil { return err } continue } log.Debug().Msgf("Extracting file %s", path) file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) if err != nil { return err } defer file.Close() if _, err = io.Copy(file, tarReader); err != nil { return err } } return nil } // Object holding a .tar.gz to write it to a file. type TarGz struct { fileWriter *os.File tarWriter *tar.Writer gzipWriter *gzip.Writer } // NewTarGz create a targz object with writers opened. // A successful call should be followed with a close. func NewTarGz(path string) (*TarGz, error) { var targz TarGz var err error targz.fileWriter, err = os.Create(path) if err != nil { return nil, fmt.Errorf("failed to write tar.gz to %s: %s", path, err) } targz.gzipWriter = gzip.NewWriter(targz.fileWriter) targz.tarWriter = tar.NewWriter(targz.gzipWriter) return &targz, nil } // Close stops all the writers. func (t *TarGz) Close() { t.tarWriter.Close() t.gzipWriter.Close() t.fileWriter.Close() } // AddFile adds the file at filepath to the archive as entrypath. func (t *TarGz) AddFile(filepath string, entrypath string) error { file, err := os.Open(filepath) if err != nil { return err } defer file.Close() info, err := file.Stat() if err != nil { return err } header, err := tar.FileInfoHeader(info, info.Name()) if err != nil { return err } header.Name = entrypath if err = t.tarWriter.WriteHeader(header); err != nil { return err } if _, err = io.Copy(t.tarWriter, file); err != nil { return err } return nil } 070701000000E8000081B400000000000000000000000165DF469000000E75000000000000000000000000000000000000002500000000uyuni-tools/shared/utils/tar_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "os" "os/exec" "path" "testing" ) const dataDir = "data" const outDir = "out" const file1_content = "file1 content" var filesData = map[string]string{ "file1": file1_content, "sub/file2": "file2 content", } // Prepare test files to include in the tarball. func setup(t *testing.T) (string, func(t *testing.T)) { dir, err := os.MkdirTemp("", "uyuni-tools-test-") if err != nil { t.Fatalf("failed to create temporary directory for test: %s", err) } // Create sub directories for the data and the test for _, dirPath := range []string{dataDir, outDir} { subDir := path.Join(dir, dirPath) if err := os.Mkdir(subDir, 0700); err != nil { t.Fatalf("failed to create %s directory: %s", dirPath, err) } } // Add some content to the data directory for name, content := range filesData { filePath := path.Dir(name) if filePath != "." { absDir := path.Join(dir, dataDir, filePath) if err := os.MkdirAll(absDir, 0700); err != nil { t.Fatalf("failed to create subdirectory %s for test: %s", absDir, err) } } if err := os.WriteFile(path.Join(dir, dataDir, name), []byte(content), 0700); err != nil { t.Fatalf("failed to write test data file %s: %s", name, err) } } // Returns the teardown function. return dir, func(t *testing.T) { if err := os.RemoveAll(dir); err != nil { t.Logf("failed to clean test directory: %s", err) } } } func TestWriteTarGz(t *testing.T) { tmpDir, teardown := setup(t) defer teardown(t) // Create the tarball tarballPath := path.Join(tmpDir, "test.tar.gz") tarball, err := NewTarGz(tarballPath) if err != nil { t.Fatalf("failed to create tarball: %s", err) } if err := tarball.AddFile(path.Join(tmpDir, dataDir, "file1"), "otherfile1"); err != nil { t.Fatalf("failed to add file1 to tarball: %s", err) } if err := tarball.AddFile(path.Join(tmpDir, dataDir, "sub/file2"), "sub/file2"); err != nil { t.Fatalf("failed to add sub/file2 to tarball: %s", err) } tarball.Close() // Check the tarball using the tar utility testDir := path.Join(tmpDir, outDir) if out, err := exec.Command("tar", "xzf", tarballPath, "-C", testDir).CombinedOutput(); err != nil { t.Fatalf("failed to extract generated tarball: %s", string(out)) } // Ensure we have all expected files for _, file := range []string{"otherfile1", "sub/file2"} { if !FileExists(path.Join(testDir, file)) { t.Errorf("Missing %s in archive", file) } } // Check the content of a file if out, err := os.ReadFile(path.Join(testDir, "otherfile1")); err != nil { t.Errorf("failed to read otherfile1: %s", err) } else if string(out) != file1_content { t.Errorf("expected otherfile1 content %s, but got %s", file1_content, string(out)) } } func TestExtractTarGz(t *testing.T) { tmpDir, teardown := setup(t) defer teardown(t) // Create an archive using the tar tool tarballPath := path.Join(tmpDir, "test.tar.gz") dataPath := path.Join(tmpDir, dataDir) if out, err := exec.Command("tar", "czf", tarballPath, "-C", dataPath, ".").CombinedOutput(); err != nil { t.Fatalf("failed to create test tar.gz: %s", string(out)) } // Extract the tarball testDir := path.Join(tmpDir, outDir) if err := ExtractTarGz(tarballPath, testDir); err != nil { t.Errorf("Failed to extract tar.gz: %s", err) } // Check the extracted content for name, content := range filesData { if out, err := os.ReadFile(path.Join(testDir, name)); err != nil { t.Errorf("failed to read %s: %s", name, err) } else if string(out) != content { t.Errorf("expected %s content %s, but got %s", name, content, string(out)) } } } 070701000000E9000081B400000000000000000000000165DF469000000332000000000000000000000000000000000000002500000000uyuni-tools/shared/utils/template.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "io" "os" "github.com/rs/zerolog/log" ) // Template is an interface for implementing Render function. type Template interface { Render(wr io.Writer) error } // WriteTemplateToFile writes a template to a file. func WriteTemplateToFile(template Template, path string, perm os.FileMode, overwrite bool) error { // Check if the file is existing if !overwrite { if FileExists(path) { log.Fatal().Msgf("%s file already present, not overwriting", path) } } // Write the configuration file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) if err != nil { log.Fatal().Err(err).Msgf("Failed to open %s for writing", path) } defer file.Close() return template.Render(file) } 070701000000EA000081B400000000000000000000000165DF469000000C65000000000000000000000000000000000000002200000000uyuni-tools/shared/utils/utils.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import ( "bufio" "fmt" "os" "regexp" "strings" "syscall" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "golang.org/x/term" ) const prompt_end = ": " // AskPasswordIfMissing asks for password if missing. func AskPasswordIfMissing(value *string, prompt string) { if *value == "" { fmt.Print(prompt + prompt_end) bytePassword, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { log.Fatal().Err(err).Msgf("Failed to read password") } *value = string(bytePassword) fmt.Println() } } // AskIfMissing asks for a value if missing. func AskIfMissing(value *string, prompt string) { if *value == "" { fmt.Print(prompt + prompt_end) reader := bufio.NewReader(os.Stdin) newValue, err := reader.ReadString('\n') if err != nil { log.Fatal().Err(err).Msgf("Failed to read input") } *value = newValue fmt.Println() } } // ComputeImage assembles the container image from its name and tag. func ComputeImage(name string, tag string, appendToName ...string) (string, error) { imageValid := regexp.MustCompile("^((?:[^:/]+(?::[0-9]+)?/)?[^:]+)(?::([^:]+))?$") submatches := imageValid.FindStringSubmatch(name) if submatches == nil { return "", fmt.Errorf("invalid image name: %s", name) } if submatches[2] == `` { if len(tag) <= 0 { return name, fmt.Errorf("tag missing on %s", name) } if len(appendToName) > 0 { name = name + strings.Join(appendToName, ``) } // No tag provided in the URL name, append the one passed imageName := fmt.Sprintf("%s:%s", name, tag) log.Debug().Msgf("Computed image name is %s", imageName) return imageName, nil } imageName := submatches[1] + strings.Join(appendToName, ``) + `:` + submatches[2] log.Debug().Msgf("Computed image name is %s", imageName) return imageName, nil } // Get the timezone set on the machine running the tool. func GetLocalTimezone() string { out, err := RunCmdOutput(zerolog.DebugLevel, "timedatectl", "show", "--value", "-p", "Timezone") if err != nil { log.Fatal().Err(err).Msgf("Failed to run timedatectl show --value -p Timezone") } return string(out) } // Check if a given path exists. func FileExists(path string) bool { _, err := os.Stat(path) if err == nil { return true } else if !os.IsNotExist(err) { log.Fatal().Err(err).Msgf("Failed to stat %s file", path) } return false } // Returns the content of a file and exit if there was an error. func ReadFile(file string) []byte { out, err := os.ReadFile(file) if err != nil { log.Fatal().Err(err).Msgf("Failed to read file %s", file) } return out } // Get the value of a file containing a boolean. // This is handy for files from the kernel API. func GetFileBoolean(file string) bool { return string(ReadFile(file)) != "0" } // Uninstalls a file. func UninstallFile(path string, dryRun bool) { if FileExists(path) { if dryRun { log.Info().Msgf("Would remove file %s", path) } else { log.Info().Msgf("Removing file %s", path) if err := os.Remove(path); err != nil { log.Info().Err(err).Msgf("Failed to remove file %s", path) } } } } 070701000000EB000081B400000000000000000000000165DF469000000633000000000000000000000000000000000000002700000000uyuni-tools/shared/utils/utils_test.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import "testing" func TestComputeImage(t *testing.T) { data := [][]string{ {"registry:5000/path/to/image:foo", "registry:5000/path/to/image:foo", "bar"}, {"registry:5000/path/to/image:bar", "registry:5000/path/to/image", "bar"}, {"registry/path/to/image:foo", "registry/path/to/image:foo", "bar"}, {"registry/path/to/image:bar", "registry/path/to/image", "bar"}, {"registry:5000/path/to/image-migration-14-16:foo", "registry:5000/path/to/image:foo", "bar", "-migration-14-16"}, {"registry:5000/path/to/image-migration-14-16:bar", "registry:5000/path/to/image", "bar", "-migration-14-16"}, {"registry/path/to/image-migration-14-16:foo", "registry/path/to/image:foo", "bar", "-migration-14-16"}, {"registry/path/to/image-migration-14-16:bar", "registry/path/to/image", "bar", "-migration-14-16"}, } for i, testCase := range data { result := testCase[0] image := testCase[1] tag := testCase[2] appendToImage := testCase[3:] actual, err := ComputeImage(image, tag, appendToImage...) if err != nil { t.Errorf("Testcase %d: Unexpected error while computing image with %s, %s, %s: %s", i, image, tag, appendToImage, err) } if actual != result { t.Errorf("Testcase %d: Expected %s got %s when computing image with %s, %s, %s", i, result, actual, image, tag, appendToImage) } } } func TestComputeImageError(t *testing.T) { _, err := ComputeImage("registry:path/to/image:tag:tag", "bar") if err == nil { t.Error("Expected error, got none") } } 070701000000EC000081B400000000000000000000000165DF469000001743000000000000000000000000000000000000002400000000uyuni-tools/shared/utils/volumes.go// SPDX-FileCopyrightText: 2024 SUSE LLC // // SPDX-License-Identifier: Apache-2.0 package utils import "github.com/uyuni-project/uyuni-tools/shared/types" // PgsqlRequiredVolumeMounts represents volumes mount used by PostgreSQL. var PgsqlRequiredVolumeMounts = []types.VolumeMount{ {MountPath: "/etc/pki/tls", Name: "etc-tls"}, {MountPath: "/var/lib/pgsql", Name: "var-pgsql"}, {MountPath: "/etc/rhn", Name: "etc-rhn"}, {MountPath: "/etc/pki/spacewalk-tls", Name: "tls-key"}, } // PgsqlRequiredVolumes represents volumes used by PostgreSQL. var PgsqlRequiredVolumes = []types.Volume{ {Name: "etc-tls", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-tls"}}, {Name: "var-pgsql", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-pgsql"}}, {Name: "etc-rhn", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-rhn"}}, {Name: "tls-key", Secret: &types.Secret{ SecretName: "uyuni-cert", Items: []types.SecretItem{ {Key: "tls.crt", Path: "spacewalk.crt"}, {Key: "tls.key", Path: "spacewalk.key"}, }, }, }, } // EtcServerVolumeMounts represents volumes mounted in /etc folder. var EtcServerVolumeMounts = []types.VolumeMount{ {MountPath: "/etc/apache2", Name: "etc-apache2"}, {MountPath: "/etc/systemd/system/multi-user.target.wants", Name: "etc-systemd-multi"}, {MountPath: "/etc/systemd/system/sockets.target.wants", Name: "etc-systemd-sockets"}, {MountPath: "/etc/salt", Name: "etc-salt"}, {MountPath: "/etc/rhn", Name: "etc-rhn"}, {MountPath: "/etc/tomcat", Name: "etc-tomcat"}, {MountPath: "/etc/cobbler", Name: "etc-cobbler"}, {MountPath: "/etc/sysconfig", Name: "etc-sysconfig"}, {MountPath: "/etc/postfix", Name: "etc-postfix"}, } // EtcServerVolumeMounts represents volumes used for configuration. var EtcServerVolumes = []types.Volume{ {Name: "etc-apache2", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-apache2"}}, {Name: "etc-systemd-multi", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-systemd-multi"}}, {Name: "etc-systemd-sockets", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-systemd-sockets"}}, {Name: "etc-salt", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-salt"}}, {Name: "etc-tomcat", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-tomcat"}}, {Name: "etc-cobbler", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-cobbler"}}, {Name: "etc-sysconfig", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-sysconfig"}}, {Name: "etc-postfix", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-postfix"}}, {Name: "etc-rhn", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "etc-rhn"}}, } var etcAndPgsqlVolumeMounts = append(PgsqlRequiredVolumeMounts, EtcServerVolumeMounts[:]...) var etcAndPgsqlVolumes = append(PgsqlRequiredVolumes, EtcServerVolumes[:]...) // ServerVolumeMounts should match the volumes mapping from the container definition in both // the helm chart and the systemctl services definitions. var ServerVolumeMounts = append([]types.VolumeMount{ {MountPath: "/var/lib/cobbler", Name: "var-cobbler"}, {MountPath: "/var/lib/salt", Name: "var-salt"}, {MountPath: "/var/cache", Name: "var-cache"}, {MountPath: "/var/spacewalk", Name: "var-spacewalk"}, {MountPath: "/var/log", Name: "var-log"}, {MountPath: "/srv/salt", Name: "srv-salt"}, {MountPath: "/srv/www/", Name: "srv-www"}, {MountPath: "/srv/tftpboot", Name: "srv-tftpboot"}, {MountPath: "/srv/formula_metadata", Name: "srv-formulametadata"}, {MountPath: "/srv/pillar", Name: "srv-pillar"}, {MountPath: "/srv/susemanager", Name: "srv-susemanager"}, {MountPath: "/srv/spacewalk", Name: "srv-spacewalk"}, {MountPath: "/root", Name: "root"}, {MountPath: "/etc/pki/trust/anchors", Name: "ca-cert"}, }, etcAndPgsqlVolumeMounts[:]...) // ServerVolumes match the volume with Persistent Volume Claim. var ServerVolumes = append([]types.Volume{ {Name: "var-cobbler", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-cobbler"}}, {Name: "var-salt", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-salt"}}, {Name: "var-cache", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-cache"}}, {Name: "var-spacewalk", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-spacewalk"}}, {Name: "var-log", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "var-log"}}, {Name: "srv-salt", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-salt"}}, {Name: "srv-www", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-www"}}, {Name: "srv-tftpboot", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-tftpboot"}}, {Name: "srv-formulametadata", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-formulametadata"}}, {Name: "srv-pillar", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-pillar"}}, {Name: "srv-susemanager", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-susemanager"}}, {Name: "srv-spacewalk", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "srv-spacewalk"}}, {Name: "root", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "root"}}, {Name: "ca-cert", PersistentVolumeClaim: &types.PersistentVolumeClaim{ClaimName: "ca-cert"}}, }, etcAndPgsqlVolumes[:]...) // PROXY_HTTPD_VOLUMES volumes used by HTTPD in proxy. var PROXY_HTTPD_VOLUMES = map[string]string{ "uyuni-proxy-rhn-cache": "/var/cache/rhn", "uyuni-proxy-tftpboot": "/srv/tftpboot", } // PROXY_HTTPD_VOLUMES volumes used by Squid in proxy. var PROXY_SQUID_VOLUMES = map[string]string{ "uyuni-proxy-squid-cache": "/var/cache/squid", } // PROXY_TFTPD_VOLUMES volumes used by TFTP in proxy. var PROXY_TFTPD_VOLUMES = map[string]string{ "uyuni-proxy-tftpboot": "/srv/tftpboot:ro", } 070701000000ED000081B400000000000000000000000165DF469000000818000000000000000000000000000000000000002000000000uyuni-tools/uyuni-tools.changes------------------------------------------------------------------- Tue Feb 13 18:45:11 CET 2024 - marina.latini@suse.com - version 0.1.4-1 * Add mgradm start stop and restart commands * Do not build fish shell completion on Red Hat Enterprise Linux and clones * Stop services and database in podman server gracefully * tomcat and taskomatic should listen on all interfaces also in podman case ------------------------------------------------------------------- Wed Jan 31 14:56:34 CET 2024 - rosuna@suse.com - version 0.1.3-1 * Add configuration help * Add a warning message for interactive shell * Accept image URLs with the tag already appended * Add mgradm supportconfig command * Verify if podman, kubectl or helm are installed before using them * Add migration of config files * Disable SELinux relabeling by Podman for migration container. Fixes SELinux access problems for SSH agent socket. * FQDN optional in command install for Podman ------------------------------------------------------------------- Mon Jan 15 11:08:45 CET 2024 - marina.latini@suse.com - version 0.1.2-1 * Adapt the build tags also in the spec file ------------------------------------------------------------------- Thu Jan 11 16:49:18 CET 2024 - marina.latini@suse.com - version 0.1.1-1 * Use tito for releasing * Use the latest git tag as version instead of hardcoding it * Comply to reuse.software rules for license documentation * Add shell autocompletions * Rename the tools to mgradm and mgrctl * Add postgres migration * Add migration of autoinstallable distributions * Add mgrpxy tool with install and uninstall subcommands * Merge /srv/www/ volumes and add one for /var/lib/salt * Build uyuniadm also for Tumbleweed and ALP ------------------------------------------------------------------- Tue Oct 24 13:24:46 UTC 2023 - Michele Bussolotto <michele.bussolotto@suse.com> - Initial packaging of uyuni-tools 0.0.3 * Create uyuniadm and uyunictl packages * Make it possible to build uyuniadm only on specific distro 070701000000EE000081B400000000000000000000000165DF469000000022000000000000000000000000000000000000003E00000000uyuni-tools/uyuni-tools.changes.mbussolotto.changelog_upgrade- add inspect and upgrade command 070701000000EF000081B400000000000000000000000165DF46900000002D000000000000000000000000000000000000003F00000000uyuni-tools/uyuni-tools.changes.mbussolotto.changelog_workflow- add github workflow for checking changelog 070701000000F0000081B400000000000000000000000165DF46900000002B000000000000000000000000000000000000004400000000uyuni-tools/uyuni-tools.changes.mbussolotto.deny_uyuni_suma_upgrade- Deny uyuni to suma upgrade and viceversa 070701000000F1000081B400000000000000000000000165DF469000000033000000000000000000000000000000000000003800000000uyuni-tools/uyuni-tools.changes.mbussolotto.fix_network- Improve error handling when exec.Command is used 070701000000F2000081B400000000000000000000000165DF469000000020000000000000000000000000000000000000003300000000uyuni-tools/uyuni-tools.changes.mbussolotto.mgrctl- ignore error on optional flag 070701000000F3000081B400000000000000000000000165DF469000000044000000000000000000000000000000000000004200000000uyuni-tools/uyuni-tools.changes.mbussolotto.set_cobbler_localhost- refactor upgrade to clarify script end adding post upgrade script 070701000000F4000081B400000000000000000000000165DF46900000002D000000000000000000000000000000000000004200000000uyuni-tools/uyuni-tools.changes.mbussolotto.start_stop_kubernetes- Start/Stop/Restart command with kubernetes 070701000000F5000081B400000000000000000000000165DF469000002B21000000000000000000000000000000000000001D00000000uyuni-tools/uyuni-tools.spec# # spec file for package uyuni-tools # # Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed # upon. The license for this file, and modifications and additions to the # file, is the same license as for the pristine package itself (unless the # license for the pristine package is not an Open Source License, in which # case the license is the MIT License). An "Open Source License" is a # license that conforms to the Open Source Definition (Version 1.9) # published by the Open Source Initiative. # Please submit bugfixes or comments via https://bugs.opensuse.org/ # %global provider github %global provider_tld com %global org uyuni-project %global project uyuni-tools %global provider_prefix %{provider}.%{provider_tld}/%{org}/%{project} %global productname Uyuni %global namespace registry.opensuse.org/uyuni %if 0%{?suse_version} >= 1600 || 0%{?sle_version} >= 150400 || 0%{?rhel} >= 8 || 0%{?fedora} >= 37 || 0%{?debian} >= 12 || 0%{?ubuntu} >= 2004 %define adm_build 1 %else %define adm_build 0 %endif %define name_adm mgradm %define name_ctl mgrctl %define name_pxy mgrpxy # Completion files %if 0%{?debian} || 0%{?ubuntu} %define _zshdir %{_datarootdir}/zsh/vendor-completions %else %define _zshdir %{_datarootdir}/zsh/site-functions %endif Name: %{project} Version: 0.1.4 Release: 1 Summary: Tools for managing %{productname} container License: Apache-2.0 Group: System/Management URL: https://%{provider_prefix} Source0: %{name}-%{version}.tar.gz Source1: vendor.tar.gz BuildRequires: bash-completion BuildRequires: coreutils %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} BuildRequires: fish %endif BuildRequires: zsh # Get the proper Go version on different distros %if 0%{?suse_version} BuildRequires: golang(API) >= 1.20 %endif %if 0%{?ubuntu} %define go_version 1.20 BuildRequires: golang-%{go_version} %endif %if 0%{?debian} BuildRequires: golang >= 1.20 %endif %if 0%{?fedora} || 0%{?rhel} BuildRequires: golang >= 1.19 %endif %description Tools for managing uyuni container. %if %{adm_build} %package -n %{name_adm} Summary: Command line tool to install and update %{productname} %description -n %{name_adm} %{name_adm} is a convenient tool to install and update %{productname} components as containers running either on Podman or a Kubernetes cluster. %package -n %{name_pxy} Summary: Command line tool to install and update %{productname} proxy %description -n %{name_pxy} %{name_pxy} is a convenient tool to install and update %{productname} proxy components as containers running either on Podman or a Kubernetes cluster. %package -n %{name_adm}-bash-completion Summary: Bash Completion for %{name_adm} Group: System/Shells Requires: %{name_adm} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_adm} and bash-completion) %else Supplements: bash-completion %endif BuildArch: noarch %description -n %{name_adm}-bash-completion Bash command line completion support for %{name_adm}. %package -n %{name_adm}-zsh-completion Summary: Zsh Completion for %{name_adm} Group: System/Shells Requires: %{name_adm} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_adm} and zsh) %else Supplements: zsh %endif BuildArch: noarch %description -n %{name_adm}-zsh-completion Zsh command line completion support for %{name_adm}. %package -n %{name_pxy}-bash-completion Summary: Bash Completion for %{name_pxy} Group: System/Shells Requires: %{name_pxy} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_pxy} and bash-completion) %else Supplements: bash-completion %endif BuildArch: noarch %description -n %{name_pxy}-bash-completion Bash command line completion support for %{name_pxy}. %package -n %{name_pxy}-zsh-completion Summary: Zsh Completion for %{name_pxy} Group: System/Shells Requires: %{name_pxy} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_pxy} and zsh) %else Supplements: zsh %endif BuildArch: noarch %description -n %{name_pxy}-zsh-completion Zsh command line completion support for %{name_pxy}. %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} %package -n %{name_adm}-fish-completion Summary: Fish Completion for %{name_adm} Group: System/Shells Requires: %{name_adm} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_adm} and fish) %else Supplements: fish %endif BuildArch: noarch %description -n %{name_adm}-fish-completion Fish command line completion support for %{name_adm}. %package -n %{name_pxy}-fish-completion Summary: Fish Completion for %{name_pxy} Group: System/Shells Requires: %{name_pxy} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_pxy} and fish) %else Supplements: fish %endif BuildArch: noarch %description -n %{name_pxy}-fish-completion Fish command line completion support for %{name_pxy}. %endif %endif %package -n %{name_ctl} Summary: Command line tool to perform day-to-day operations on %{productname} %description -n %{name_ctl} %{name_ctl} is a tool helping with dayly tasks on %{productname} components running as containers either on Podman or a Kubernetes cluster. %package -n %{name_ctl}-bash-completion Summary: Bash Completion for %{name_ctl} Group: System/Shells Requires: %{name_ctl} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_ctl} and bash-completion) %else Supplements: bash-completion %endif BuildArch: noarch %description -n %{name_ctl}-bash-completion Bash command line completion support for %{name_ctl}. %package -n %{name_ctl}-zsh-completion Summary: Zsh Completion for %{name_ctl} Group: System/Shells Requires: %{name_ctl} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_ctl} and zsh) %else Supplements: zsh %endif BuildArch: noarch %description -n %{name_ctl}-zsh-completion Zsh command line completion support for %{name_ctl}. %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} %package -n %{name_ctl}-fish-completion Summary: Fish Completion for %{name_ctl} Group: System/Shells Requires: %{name_ctl} = %{version} %if 0%{?suse_version} >= 150000 Supplements: (%{name_ctl} and fish) %else Supplements: fish %endif BuildArch: noarch %description -n %{name_ctl}-fish-completion Fish command line completion support for %{name_ctl}. %endif %prep %autosetup tar -zxf %{SOURCE1} %build export GOFLAGS=-mod=vendor mkdir -p bin UTILS_PATH="%{provider_prefix}/shared/utils" tag=%{!?_default_tag:latest} %if "%{?_default_tag}" != "" tag='%{_default_tag}' %endif image=%{namespace} %if "%{?_default_namespace}" != "" namespace='%{_default_namespace}' %endif go_tags="" %if "%{?_uyuni_tools_tags}" != "" go_tags="-tags %{_uyuni_tools_tags}" %endif go_path= %if 0%{?ubuntu} go_path=/usr/lib/go-%{go_version}/bin/ %else %if "%{?_go_bin}" != "" go_path='%{_go_bin}/' %endif %endif GOLD_FLAGS="-X ${UTILS_PATH}.Version=%{version}" if test -n "${namespace}"; then GOLD_FLAGS="-X ${UTILS_PATH}.DefaultNamespace=${namespace} -X ${UTILS_PATH}.DefaultTag=${tag}" fi if test -n "${tag}"; then GOLD_FLAGS="${GOLD_FLAGS} -X ${UTILS_PATH}.DefaultTag=${tag}" fi # Workaround for rpm on Fedora and EL clones not able to handle go's compressed debug symbols # Found compressed .debug_aranges section, not attempting dwz compression %if 0%{?rhel} >= 8 || 0%{?fedora} >= 38 GOLD_FLAGS="-compressdwarf=false ${GOLD_FLAGS}" %endif # Workaround for missing build-id on Fedora # error: Missing build-id in [...] %if 0%{?fedora} >= 38 GOLD_FLAGS="-B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \n') ${GOLD_FLAGS}" %endif ${go_path}go build ${go_tags} -ldflags "${GOLD_FLAGS}" -o ./bin ./... %if ! %{adm_build} rm ./bin/%{name_adm} rm ./bin/%{name_pxy} %endif %install install -m 0755 -vd %{buildroot}%{_bindir} install -m 0755 -vp ./bin/* %{buildroot}%{_bindir}/ # Completion files mkdir -p %{buildroot}%{_datarootdir}/bash-completion/completions/ mkdir -p %{buildroot}%{_zshdir} %{buildroot}/%{_bindir}/%{name_ctl} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name_ctl} %{buildroot}/%{_bindir}/%{name_ctl} completion zsh > %{buildroot}%{_zshdir}/_%{name_ctl} %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} mkdir -p %{buildroot}%{_datarootdir}/fish/vendor_completions.d/ %{buildroot}/%{_bindir}/%{name_ctl} completion fish > %{buildroot}%{_datarootdir}/fish/vendor_completions.d/%{name_ctl}.fish %endif %if %{adm_build} %{buildroot}/%{_bindir}/%{name_adm} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name_adm} %{buildroot}/%{_bindir}/%{name_adm} completion zsh > %{buildroot}%{_zshdir}/_%{name_adm} %{buildroot}/%{_bindir}/%{name_pxy} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name_pxy} %{buildroot}/%{_bindir}/%{name_pxy} completion zsh > %{buildroot}%{_zshdir}/_%{name_pxy} %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} %{buildroot}/%{_bindir}/%{name_adm} completion fish > %{buildroot}%{_datarootdir}/fish/vendor_completions.d/%{name_adm}.fish %{buildroot}/%{_bindir}/%{name_pxy} completion fish > %{buildroot}%{_datarootdir}/fish/vendor_completions.d/%{name_pxy}.fish %endif %endif %if %{adm_build} %files -n %{name_adm} %defattr(-,root,root) %doc README.md %license LICENSE %{_bindir}/%{name_adm} %files -n %{name_adm}-bash-completion %{_datarootdir}/bash-completion/completions/%{name_adm} %files -n %{name_adm}-zsh-completion %{_zshdir}/_%{name_adm} %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} %files -n %{name_adm}-fish-completion %{_datarootdir}/fish/vendor_completions.d/%{name_adm}.fish %endif %files -n %{name_pxy} %defattr(-,root,root) %doc README.md %license LICENSE %{_bindir}/%{name_pxy} %files -n %{name_pxy}-bash-completion %{_datarootdir}/bash-completion/completions/%{name_pxy} %files -n %{name_pxy}-zsh-completion %{_zshdir}/_%{name_pxy} %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} %files -n %{name_pxy}-fish-completion %{_datarootdir}/fish/vendor_completions.d/%{name_pxy}.fish %endif %endif %files -n %{name_ctl} %defattr(-,root,root) %doc README.md %license LICENSE %{_bindir}/%{name_ctl} %files -n %{name_ctl}-bash-completion %{_datarootdir}/bash-completion/completions/%{name_ctl} %files -n %{name_ctl}-zsh-completion %{_zshdir}/_%{name_ctl} %if 0%{?is_opensuse} || 0%{?fedora} || 0%{?debian} || 0%{?ubuntu} %files -n %{name_ctl}-fish-completion %{_datarootdir}/fish/vendor_completions.d/%{name_ctl}.fish %endif %changelog 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!
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