├── .gitignore ├── MANIFEST.in ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── AUTHORS ├── Makefile ├── setup.py ├── bootstrap.yml ├── CONTRIBUTING.md ├── pylintrc ├── LICENSE ├── README.md └── bootstrap.py /.gitignore: -------------------------------------------------------------------------------- 1 | bootstrap.pyc 2 | dist/ 3 | MANIFEST 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include bootstrap.yml 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | target: 12 | - "lint" 13 | - "test-centos:6" 14 | - "test-centos:7" 15 | - "test3-centos:stream8" 16 | - "test3-centos:stream9" 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | if: matrix.target == 'lint' 22 | - name: Install 23 | run: pip install flake8 'pylint<2.13' 'astroid<2.13' 'isort<5' 24 | if: matrix.target == 'lint' 25 | - name: Run tests 26 | run: make ${{ matrix.target }} 27 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | AUTHORS 2 | ======= 3 | 4 | Daniel Lobato Garcia 5 | Daniele Palumbo 6 | David Kaylor 7 | Eric D Helms 8 | Eric Lavarde 9 | Evgeni Golov 10 | François Cami 11 | Johan Bergström 12 | Johan Swensson 13 | Joseph Pisciotta 14 | karmab 15 | Lukas Zapletal 16 | Marcelo Moreira de Mello 17 | Mike McCune 18 | Rich Jerrido 19 | Roman Plevka 20 | Simon Piette 21 | Vagner Farias 22 | Yannick Charton 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER=docker 2 | TEST_TARGETS=centos\:6 centos\:7 3 | TEST3_TARGETS=centos\:stream8 centos\:stream9 4 | USE_SELINUX=$(shell test -d /sys/fs/selinux && echo ":Z") 5 | 6 | test: $(addprefix test-,$(TEST_TARGETS)) $(addprefix test3-,$(TEST3_TARGETS)) lint 7 | 8 | test-%: 9 | $(DOCKER) run -i --volume $(CURDIR):/app$(USE_SELINUX) --workdir=/app quay.io/centos/$* python bootstrap.py --help 10 | $(DOCKER) run -i --volume $(CURDIR):/app$(USE_SELINUX) --workdir=/app quay.io/centos/$* python setup.py sdist 11 | 12 | test3-%: 13 | $(DOCKER) run -i --volume $(CURDIR):/app$(USE_SELINUX) --workdir=/app quay.io/centos/$* /usr/libexec/platform-python bootstrap.py --help 14 | $(DOCKER) run -i --volume $(CURDIR):/app$(USE_SELINUX) --workdir=/app quay.io/centos/$* /usr/libexec/platform-python setup.py sdist 15 | 16 | lint: 17 | python -m flake8 --ignore E501,W504 ./bootstrap.py ./setup.py 18 | python -m pylint --reports=n --disable=I --ignore-imports=y ./bootstrap.py ./setup.py 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """setup script for katello-client-bootstrap""" 4 | 5 | from distutils.core import setup # pylint:disable=import-error,no-name-in-module 6 | from bootstrap import VERSION 7 | 8 | setup( 9 | name='katello-client-bootstrap', 10 | version=VERSION, 11 | description='Bootstrap Script for migrating systems to Foreman & Katello', 12 | author='Rich Jerrido', 13 | author_email='rjerrido@outsidaz.org', 14 | license='GPL-2', 15 | url='https://github.com/Katello/katello-client-bootstrap', 16 | scripts=['bootstrap.py'], 17 | classifiers=[ 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Intended Audience :: System Administrators', 20 | 'Topic :: System :: Systems Administration', 21 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 22 | 'Operating System :: POSIX :: Linux', 23 | 'Programming Language :: Python :: 2', 24 | 'Programming Language :: Python :: 2.6', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.8', 29 | 'Programming Language :: Python :: 3.9', 30 | ], 31 | ) 32 | -------------------------------------------------------------------------------- /bootstrap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | vars: 4 | bootstrap_foreman_fqdn: foreman.example.com 5 | bootstrap_download_method: http 6 | bootstrap_org: "Default Organization" 7 | bootstrap_location: "World" 8 | bootstrap_activationkey: "ActivationKey" 9 | bootstrap_login: "" 10 | bootstrap_password: "" 11 | bootstrap_hostgroup: "HostGroup" 12 | bootstrap_additional_args: "" 13 | bootstrap_target_path: /root/bootstrap.py 14 | bootstrap_local_copy: False 15 | tasks: 16 | - name: download bootstrap.py from {{ bootstrap_foreman_fqdn }} 17 | get_url: 18 | dest: "{{ bootstrap_target_path }}" 19 | url: "{{ bootstrap_download_method }}://{{ bootstrap_foreman_fqdn }}/pub/bootstrap.py" 20 | when: not bootstrap_local_copy 21 | 22 | - name: copy bootstrap.py from local machine 23 | copy: 24 | dest: "{{ bootstrap_target_path }}" 25 | src: bootstrap.py 26 | when: bootstrap_local_copy 27 | 28 | - name: generate bootstrap.py arguments 29 | set_fact: 30 | bootstrap_args: "--server {{ bootstrap_foreman_fqdn }} --organization '{{ bootstrap_org }}' --location '{{ bootstrap_location }}' --activationkey '{{ bootstrap_activationkey }}' --download-method {{ bootstrap_download_method }}" 31 | bootstrap_foreman_args: "{% if bootstrap_password != '' %}--login '{{ bootstrap_login }}' --password '{{ bootstrap_password }}' --hostgroup '{{ bootstrap_hostgroup }}'{% else %}--skip foreman{% endif %}" 32 | 33 | - name: run bootstrap.py 34 | command: "python {{ bootstrap_target_path }} {{ bootstrap_args }} {{ bootstrap_foreman_args }} {{ bootstrap_additional_args }}" 35 | no_log: True 36 | 37 | - name: remove bootstrap.py 38 | file: 39 | path: "{{ bootstrap_target_path }}" 40 | state: absent 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to katello-client-bootstrap 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to **katello-client-bootstrap** and which is hosted on GitHub in the [Katello](https://github.com/Katello) Organization. 6 | These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 7 | 8 | ## How Can I Contribute? 9 | 10 | ### Reporting Bugs 11 | 12 | This section guides you through submitting a bug report for katello-client-bootstrap. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. 13 | 14 | 15 | #### How Do I Submit A (Good) Bug Report? 16 | 17 | Bugs are tracked as [GitHub issues](https://github.com/Katello/katello-client-bootstrap/issues). 18 | 19 | Explain the problem and include additional details to help maintainers reproduce the problem: 20 | 21 | * **Use a clear and descriptive title** for the issue to identify the problem. 22 | * **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started , e.g. which command exactly you used in the terminal, or how you started `bootstrap.py` otherwise. When listing steps, **don't just say what you did, but explain how you did it**. 23 | 24 | * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). Note: some of the output of `bootstrap.py` contains hostnames and/or other internal information. Feel free to sanitize this data as long as the sanitized data still reproduces your problem. 25 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 26 | * **Explain which behavior you expected to see instead and why.** 27 | * **Include debugging data** 28 | - run `bootstrap.py` with the `--verbose` option 29 | - provide relevant Foreman logs (from production.log usually) 30 | - If bootstrap fails when calling an external command (such as `subscription-manager`, `puppet` or `rhn-migrate-classic-to-rhsm`), try running that command by itself with debugging/verbose options to get more logs. 31 | 32 | Provide more context by answering these questions: 33 | 34 | * **Can you reproduce the problem?** 35 | * **Did the problem start happening recently** (e.g. after updating to a new version of `bootstrap.py`) or was this always a problem? 36 | * If the problem started happening recently, **can you reproduce the problem in an older version**. i.e.: What's the most recent version in which the problem doesn't happen? 37 | * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. 38 | 39 | Include details about your configuration and environment: 40 | 41 | ### Suggesting Enhancements 42 | 43 | Features are also tracked today as [GitHub issues](https://github.com/Katello/katello-client-bootstrap/issues). 44 | 45 | This section guides you through submitting an enhancement suggestion for **katello-client-bootstrap**, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. 46 | 47 | 48 | ### Your First Contribution 49 | 50 | Unsure where to begin contributing to **katello-client-bootstrap** ? 51 | 52 | ### Developer and contributor notes: 53 | 54 | Use `pydoc ./bootstrap.py` to get the code documentation. 55 | 56 | Use `awk -F'# >' 'NF>1 {print $2}' ./bootstrap.py` to see the flow of the script. 57 | 58 | Generally, we follow a 'fork and branch' workflow: 59 | 60 | - Fork the repository. 61 | - Clone the forked repository to your local system. 62 | - Add a Git remote for the original repository. 63 | - Create a feature branch in which to place your changes. 64 | - Make your changes to the new branch. 65 | - Commit the changes to the branch. 66 | - Push the branch to GitHub. 67 | - Open a pull request from the new branch to the original repo. 68 | - Clean up after your pull request is merged. 69 | 70 | ### Other Notes 71 | 72 | - when testing `bootstrap.py` you will register (and unregister) a lot of systems. It might be advantageous to set the `unregister_delete_host` setting (Under Administer -> Settings -> Katello). This deletes the host when unregistering via `subscription-manager` 73 | 74 | ### Styleguides 75 | 76 | - Generally follow [PEP8](https://www.python.org/dev/peps/pep-0008/) with the exception of E501 - line too long errors. 77 | 78 | ### Git Commits and Commit Messages 79 | 80 | * When adding a new feature, squash your commits. so that the feature (in its entirety) can be more easily cherry-picked. 81 | * Use the present tense ("Add feature" not "Added feature") 82 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 83 | * Limit the first line to 72 characters or less 84 | * Reference issues and pull requests liberally 85 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist= 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,superfluous-parens,consider-using-with,unspecified-encoding,consider-using-f-string 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=yes 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [VARIABLES] 100 | 101 | # Tells whether we should check for unused import in __init__ files. 102 | init-import=no 103 | 104 | # A regular expression matching the name of dummy variables (i.e. expectedly 105 | # not used). 106 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 107 | 108 | # List of additional names supposed to be defined in builtins. Remember that 109 | # you should avoid to define new builtins when possible. 110 | additional-builtins= 111 | 112 | # List of strings which can identify a callback function by name. A callback 113 | # name must start or end with one of those strings. 114 | callbacks=cb_,_cb 115 | 116 | # List of qualified module names which can have objects that can redefine 117 | # builtins. 118 | redefining-builtins-modules=six.moves,future.builtins 119 | 120 | 121 | [FORMAT] 122 | 123 | # Maximum number of characters on a single line. 124 | max-line-length=250 125 | 126 | # Regexp for a line that is allowed to be longer than the limit. 127 | ignore-long-lines=^\s*(# )??$ 128 | 129 | # Allow the body of an if to be on the same line as the test if there is no 130 | # else. 131 | single-line-if-stmt=no 132 | 133 | # List of optional constructs for which whitespace checking is disabled. `dict- 134 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 135 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 136 | # `empty-line` allows space-only lines. 137 | no-space-check=trailing-comma,dict-separator 138 | 139 | # Maximum number of lines in a module 140 | max-module-lines=2000 141 | 142 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 143 | # tab). 144 | indent-string=' ' 145 | 146 | # Number of spaces of indent required inside a hanging or continued line. 147 | indent-after-paren=4 148 | 149 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 150 | expected-line-ending-format= 151 | 152 | 153 | [TYPECHECK] 154 | 155 | # Tells whether missing members accessed in mixin class should be ignored. A 156 | # mixin class is detected if its name ends with "mixin" (case insensitive). 157 | ignore-mixin-members=yes 158 | 159 | # List of module names for which member attributes should not be checked 160 | # (useful for modules/projects where namespaces are manipulated during runtime 161 | # and thus existing member attributes cannot be deduced by static analysis. It 162 | # supports qualified module names, as well as Unix pattern matching. 163 | ignored-modules= 164 | 165 | # List of class names for which member attributes should not be checked (useful 166 | # for classes with dynamically set attributes). This supports the use of 167 | # qualified names. 168 | ignored-classes=optparse.Values,thread._local,_thread._local 169 | 170 | # List of members which are set dynamically and missed by pylint inference 171 | # system, and so shouldn't trigger E1101 when accessed. Python regular 172 | # expressions are accepted. 173 | generated-members= 174 | 175 | # List of decorators that produce context managers, such as 176 | # contextlib.contextmanager. Add to this list to register other decorators that 177 | # produce valid context managers. 178 | contextmanager-decorators=contextlib.contextmanager 179 | 180 | 181 | [LOGGING] 182 | 183 | # Logging modules to check that the string format arguments are in logging 184 | # function parameter format 185 | logging-modules=logging 186 | 187 | 188 | [SPELLING] 189 | 190 | # Spelling dictionary name. Available dictionaries: en_US (myspell), en 191 | # (aspell), en_AU (aspell), en_CA (aspell), en_GB (aspell). 192 | spelling-dict= 193 | 194 | # List of comma separated words that should not be checked. 195 | spelling-ignore-words= 196 | 197 | # A path to a file that contains private dictionary; one word per line. 198 | spelling-private-dict-file= 199 | 200 | # Tells whether to store unknown words to indicated private dictionary in 201 | # --spelling-private-dict-file option instead of raising a message. 202 | spelling-store-unknown-words=no 203 | 204 | 205 | [MISCELLANEOUS] 206 | 207 | # List of note tags to take in consideration, separated by a comma. 208 | notes=FIXME,XXX,TODO 209 | 210 | 211 | [SIMILARITIES] 212 | 213 | # Minimum lines number of a similarity. 214 | min-similarity-lines=4 215 | 216 | # Ignore comments when computing similarities. 217 | ignore-comments=yes 218 | 219 | # Ignore docstrings when computing similarities. 220 | ignore-docstrings=yes 221 | 222 | # Ignore imports when computing similarities. 223 | ignore-imports=no 224 | 225 | 226 | [BASIC] 227 | 228 | # Good variable names which should always be accepted, separated by a comma 229 | good-names=i,j,k,ex,Run,_ 230 | 231 | # Bad variable names which should always be refused, separated by a comma 232 | bad-names=foo,bar,baz,toto,tutu,tata 233 | 234 | # Colon-delimited sets of names that determine each other's naming style when 235 | # the name regexes allow several styles. 236 | name-group= 237 | 238 | # Include a hint for the correct naming format with invalid-name 239 | include-naming-hint=no 240 | 241 | # List of decorators that produce properties, such as abc.abstractproperty. Add 242 | # to this list to register other decorators that produce valid properties. 243 | property-classes=abc.abstractproperty 244 | 245 | # Regular expression matching correct function names 246 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 247 | 248 | # Naming hint for function names 249 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 250 | 251 | # Regular expression matching correct variable names 252 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 253 | 254 | # Naming hint for variable names 255 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 256 | 257 | # Regular expression matching correct constant names 258 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 259 | 260 | # Naming hint for constant names 261 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 262 | 263 | # Regular expression matching correct attribute names 264 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 265 | 266 | # Naming hint for attribute names 267 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 268 | 269 | # Regular expression matching correct argument names 270 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 271 | 272 | # Naming hint for argument names 273 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 274 | 275 | # Regular expression matching correct class attribute names 276 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 277 | 278 | # Naming hint for class attribute names 279 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 280 | 281 | # Regular expression matching correct inline iteration names 282 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 283 | 284 | # Naming hint for inline iteration names 285 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 286 | 287 | # Regular expression matching correct class names 288 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 289 | 290 | # Naming hint for class names 291 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 292 | 293 | # Regular expression matching correct module names 294 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 295 | 296 | # Naming hint for module names 297 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 298 | 299 | # Regular expression matching correct method names 300 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 301 | 302 | # Naming hint for method names 303 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 304 | 305 | # Regular expression which should only match function or class names that do 306 | # not require a docstring. 307 | no-docstring-rgx=^_ 308 | 309 | # Minimum line length for functions/classes that require docstrings, shorter 310 | # ones are exempt. 311 | docstring-min-length=-1 312 | 313 | 314 | [ELIF] 315 | 316 | # Maximum number of nested blocks for function / method body 317 | max-nested-blocks=5 318 | 319 | 320 | [CLASSES] 321 | 322 | # List of method names used to declare (i.e. assign) instance attributes. 323 | defining-attr-methods=__init__,__new__,setUp 324 | 325 | # List of valid names for the first argument in a class method. 326 | valid-classmethod-first-arg=cls 327 | 328 | # List of valid names for the first argument in a metaclass class method. 329 | valid-metaclass-classmethod-first-arg=mcs 330 | 331 | # List of member names, which should be excluded from the protected access 332 | # warning. 333 | exclude-protected=_asdict,_fields,_replace,_source,_make 334 | 335 | 336 | [DESIGN] 337 | 338 | # Maximum number of arguments for function / method 339 | max-args=6 340 | 341 | # Argument names that match this expression will be ignored. Default to name 342 | # with leading underscore 343 | ignored-argument-names=_.* 344 | 345 | # Maximum number of locals for function / method body 346 | max-locals=15 347 | 348 | # Maximum number of return / yield for function / method body 349 | max-returns=6 350 | 351 | # Maximum number of branch for function / method body 352 | max-branches=15 353 | 354 | # Maximum number of statements in function / method body 355 | max-statements=50 356 | 357 | # Maximum number of parents for a class (see R0901). 358 | max-parents=7 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Minimum number of public methods for a class (see R0903). 364 | min-public-methods=2 365 | 366 | # Maximum number of public methods for a class (see R0904). 367 | max-public-methods=20 368 | 369 | # Maximum number of boolean expressions in a if statement 370 | max-bool-expr=5 371 | 372 | 373 | [IMPORTS] 374 | 375 | # Deprecated modules which should not be used, separated by a comma 376 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 377 | 378 | # Create a graph of every (i.e. internal and external) dependencies in the 379 | # given file (report RP0402 must not be disabled) 380 | import-graph= 381 | 382 | # Create a graph of external dependencies in the given file (report RP0402 must 383 | # not be disabled) 384 | ext-import-graph= 385 | 386 | # Create a graph of internal dependencies in the given file (report RP0402 must 387 | # not be disabled) 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Analyse import fallback blocks. This can be used to support both Python 2 and 398 | # 3 compatible code, which means that the block might have code that exists 399 | # only in one or another interpreter, leading to false positives when analysed. 400 | analyse-fallback-blocks=no 401 | 402 | 403 | [EXCEPTIONS] 404 | 405 | # Exceptions that will emit a warning when being caught. Defaults to 406 | # "Exception" 407 | overgeneral-exceptions=Exception 408 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foreman Bootstrap Script 2 | bootstrap Script for migrating existing running systems to Foreman with the Katello plugin 3 | 4 | # Overview 5 | 6 | * The goal is to take a Red Hat Enterprise Linux (RHEL) client and get it registered to Foreman 7 | This script can take a system that is registered to Spacewalk, Satellite 5, Red Hat 8 | Network Classic and get it registered to Foreman & Katello. 9 | * Optionally, you can also move systems between Capsules (both internal and 10 | external) of one Katello installation by using the `--new-capsule` option. 11 | 12 | # What does the Script do? 13 | 14 | * Identify which systems management platform is the system registered to (Classic/Sat5 or None) then perform the following: 15 | 16 | ## Red Hat Classic & Satellite 5 17 | 18 | * Installing subscription-manager and its pre-reqs (updated yum & openssl) 19 | * Make an API call to Katello to create the Foreman Host associated with the user specified Org/Location 20 | * Install the Katello consumer RPM 21 | * Running rhn-migrate-classic-to-rhsm (with the user provided activation key) to get product certs on a system 22 | * registering the system to Foreman 23 | * Configuring the system with a proper Puppet configuration pointing at Foreman 24 | * Removing/disabling old RHN Classic packages/daemons (rhnsd, osad, etc) 25 | 26 | ## System already registered to a Foreman + Katello server / Capsule (--new-capsule) 27 | * Clean the existing Katello agent installation 28 | * Install the Katello consumer RPM for the target Foreman + Katello server / Capsule 29 | * Install the Katello agent software again, using the configuration for the 30 | target Foreman + Katello / Capsule server 31 | * Make API calls to switch the system to a different hostgroup (optional) 32 | * Make API calls to update the Puppet master, Puppet CA, content source and 33 | OpenSCAP proxy IDs (optional, except for content source) 34 | * Re-enable rhsmcertd 35 | * Update the Puppet configuration for the system to point to the right capsule (optional) 36 | * Restart Puppet and call for the user to go sign the CSR 37 | 38 | ## System not registered to any Red Hat Systems Management Platform: 39 | 40 | * Make an API call to Foreman to create the Foreman Host associated with the user specified Org/Location 41 | * Install the Katello consumer RPM 42 | * Running subscription-manager (with the user provided activation key) to register the system. 43 | * Configuring the system with a proper Puppet configuration pointing at Foreman 44 | * Removing/disabling old RHN Classic packages/daemons (rhnsd, osad, etc) 45 | 46 | # Assumptions 47 | 48 | * The script will use only components that are present on all RHEL 49 | installations. We will not install additional packages, other than 50 | those explicitly required for Katello management, on the client 51 | system. (i.e., I could have used the python-requests module to make the 52 | API calls a lot more pleasant, but I couldn't justify the dependencies) 53 | * The system in question has python. 54 | * The administrator can approve Puppet certificates if using Puppet. 55 | Alternatively, autosigning can be enabled for the system in question. (And be careful, auto-signing isn't one of those things you'd leave enabled forever) 56 | * The Foreman instance is properly prepared and is able to provision systems, 57 | especially the following is true: 58 | * The activation key provides access to a Content View 59 | which provides Puppet and other client side tooling. 60 | * The domain of the system is known to Foreman. 61 | * If not using the `--skip foreman` option, the hostgroup has the "Host Group" and "Operating System" tabs filled out completely. Otherwise, when boostrap runs to create the host, required information will be missing and the API call with fail. 62 | 63 | # Dependencies 64 | 65 | * Python2 >= 2.5 or 2.4 with python-simplejson installed 66 | * subscription-manager (if the machine has no previous subscription) 67 | * subscription-manager-migration >= 1.14.2 (if the machine is subscribed to Satellite 5 or Red Hat Classic) 68 | 69 | # User required inputs 70 | 71 | * Hostname of Foreman and/or Capsule host 72 | * username of user with privileges to add new hosts on Foreman via the API 73 | * password of the aforementioned user. 74 | * Location and Organization that the system is to be associated with. 75 | * hostgroup that the client is to be associated with. 76 | * An Activation Key that provides a content view with access to Puppet and other tools 77 | * If `subscription-manager` is not installed or is unavailable in the host's configured repositories, a URL pointing to a repository with the `subscription-manager` RPMs is required. 78 | 79 | 80 | # Frequently asked Questions: 81 | 82 | We *strongly* recommend reading this document in its entirety _prior_ to running `bootstrap.py`. The script has a plethora of options for near any migration or Registration use-case. 83 | 84 | * **Q**: Why does `bootstrap.py` require a username and password to be used? 85 | * **A**: By default, `bootstrap.py` attempts to configure a host with a proper hostgroup, environment, organization, and location by making API calls to Foreman. The API requires authentication, and as such so does `bootstrap.py`. Alternatively, in many cases, hostgroups aren't used, and it is desired to register a host solely for content management. In this usage (when either the `--skip foreman` or `--content-only` options are provided), only an activation key. 86 | 87 | 88 | * **Q**: Why doesn't `bootstrap.py` use python-requests? 89 | * **A**: When designing `bootstrap.py` we wanted to make (and keep) the number of additional packages as minimal as possible, especially as `bootstrap.py` is only run once, it seems wasteful to install packages for a one-time use case. `bootstrap.py` assumes that only the standard python modules are available. 90 | 91 | 92 | * **Q**: Why didn't you write `bootstrap.py` in Ruby (or $OTHER language)? 93 | * **A**: Ruby is not a default package in most installations of RPM family distributions. To be as applicable to the largest number of users (and to not require a large amount of dependencies), `bootstrap.py` is written in Python. 94 | 95 | 96 | * **Q**: Why are the SSH keys for remote execution not deployed by default? 97 | * **A**: The remote execution public keys, if copied locally will allow a user with appropriate permissions to run jobs against that system. We believe that changing the security profile of an existing system should be 'opt-in', not 'opt-out'. Please pass the `--rex*` options to setup Remote Execution. 98 | 99 | 100 | * **Q**: My systems have short hostname, why does `bootstrap.py` not work properly? 101 | * **A**: Hostnames are a unique identifier within Foreman and are used in many places such as Puppet certificate generation. They are required to be FQDNs. If you have short hostnames and cannot change them, see the [**Providing an arbitrary Fully Qualified Domain Name**](#providing-an-arbitrary-fully-qualified-domain-name) section below. 102 | 103 | # Permissions 104 | 105 | The script requires certain permissions to work properly. These heavily depend on the amount of enabled features. 106 | 107 | By default you will need the following permissions: 108 | 109 | * View organizations 110 | * View locations 111 | * View domains 112 | * View subnets 113 | * View hostgroups 114 | * View hosts 115 | * View architectures 116 | * View partitiontables 117 | * View operatingsystems 118 | * Create hosts 119 | 120 | These can be easily achieved by giving the user the 'Viewer' and 'Edit hosts' roles. Please note that the 'Edit hosts' role also allows to edit and delete hosts (see below), so it might be too permissive, depending on the environment. 121 | 122 | When using the `--remove` or `--force` options, the following additional permissions are needed: 123 | 124 | * Delete hosts 125 | * Edit hosts 126 | 127 | When using the `--add-domain` option, the following additional permission is needed: 128 | 129 | * Create domains 130 | 131 | When using the `--skip foreman` or `--content-only` option, no user account in Foreman is needed at all. 132 | 133 | When using the `--legacy-purge` option, a user account on the legacy environment (RHN/Satellite5) is required. The user needs to be an admin of the system in the legacy environment by having any of the following roles: 134 | 135 | * organization administrator 136 | * system group administrator for a system group that the system is a member of 137 | * granted permissions to the system explicitly via Users-> account-> 'Systems Administered by this User' 138 | 139 | # Usage: 140 | 141 | On an EL8 (RHEL8, CentOS8, etc) host, there is no `/usr/bin/python` or `/usr/bin/python3` by default. The `bootstrap.py` script can be used with the `platform-python` as follows: 142 | ~~~ 143 | # /usr/libexec/platform-python bootstrap.py 144 | ~~~ 145 | When the `python36` module is installed, `/usr/bin/python3` can also be used. 146 | 147 | ### Registering a system to Foreman + Katello 148 | 149 | This is one of the most standard workflows with bootstrap.py. This sets up the system for content / configuration (via Puppet) & provisioning. 150 | ~~~ 151 | # ./bootstrap.py -l admin \ 152 | -s foreman.example.com \ 153 | -o "Red Hat" \ 154 | -L RDU \ 155 | -g "RHEL7/Crash" \ 156 | -a ak-Reg_To_Crash 157 | ~~~ 158 | 159 | ### Registering a system omitting Puppet setup. 160 | 161 | There are times where you wish to not install Puppet, perhaps you have a differing or existing configuration management system. 162 | 163 | ~~~ 164 | # ./bootstrap.py -l admin \ 165 | -s foreman.example.com \ 166 | -o "Red Hat" \ 167 | -L RDU \ 168 | -g "RHEL7/Crash" \ 169 | -a ak-Reg_To_Crash \ 170 | --skip puppet 171 | ~~~ 172 | 173 | ### Registering a system to Foreman + Katello, for content only. 174 | 175 | This usage leverages the `--skip foreman` switch, which does not require username/password authentication. 176 | 177 | **NOTES** 178 | 179 | - the `--skip foreman` switch implies `--skip puppet` 180 | - When using `--skip foreman`, it is expected that the organization specified (via `--organization|-o`) is specified via **LABEL**, not **NAME**. 181 | 182 | Option 1: using the `--skip foreman` option. 183 | 184 | ~~~ 185 | # ./bootstrap.py -s foreman.example.com \ 186 | -a ak_Reg_To_Dev_EL7 \ 187 | -o "Red_Hat" \ 188 | --skip foreman 189 | ~~~ 190 | 191 | Option 2 : using the `--content-only` option. This option exists as an alias to `--skip foreman`. 192 | 193 | ~~~ 194 | # ./bootstrap.py -s foreman.example.com \ 195 | -a ak_Reg_To_Dev_EL7 \ 196 | -o "Red_Hat" \ 197 | --content-only 198 | ~~~ 199 | 200 | 201 | ### Migrating a system from Red Hat Network (RHN) or Satellite 5 to Foreman 202 | 203 | bootstrap.py detects the presence of `/etc/syconfig/rhn/systemid` and a valid connection to RHN/Satellite 5 as an indicator that the system is registered to a legacy platform. In these use-cases, bootstrap will call `rhn-classic-migrate-to-rhsm` to ensure the system is migrated properly from RHN or Satellite 5. 204 | 205 | By default, bootstrap.py does not delete the system's profile from the legacy platform. This is done to keep the systems record for audit/accounting reasons. If it is desired to remove the legacy profile from RHN/Satellite 5, the `--legacy-purge` switch can be used. 206 | 207 | **NOTES**: 208 | 209 | - The `--legacy-purge` switch requires a user account on RHN/Satellite 5 with permissions to remove the systems in question. 210 | - The `--legacy-purge` switch does not work on EL5 systems, as they lack the tooling to instruct the RHN/Satellite5 API to purge the old system entry. 211 | - The `--legacy-login` and `--legacy-password` options allow the correct RHN/Satellite 5 username/password to be provided to bootstrap.py. 212 | - bootstrap.py will prompt the user for the Legacy Password if not provided via CLI parameter. 213 | - If you wish to skip the migration of the system from RHN or Satellite 5 to Foreman, pass `--skip migration` as a CLI option. 214 | 215 | 216 | ~~~ 217 | # ./bootstrap.py -l admin \ 218 | -s foreman.example.com \ 219 | -o "Red Hat" \ 220 | -L RDU \ 221 | -g "RHEL7/Crash" \ 222 | -a ak-Reg_To_Crash \ 223 | --legacy-purge \ 224 | --legacy-login rhn-user 225 | ~~~ 226 | 227 | ### Migrating a system from one Foreman + Katello installation to another. 228 | 229 | There are times where it is necessary to migrate clients from one Foreman + Katello installation to another. For instance, in lieu of upgrading an older Foreman + Katello installation, you choose to build a new installation in parallel. bootstrap.py can then be used to migrate clients from one Foreman + Katello installation to another. Simply provide the `--force` option, and `bootstrap.py` will remove the previous `katello-ca-consumer-*` package (from the old system), and will install the `katello-ca-consumer-*` package (from the new system), and continue registration as usual. 230 | 231 | ### Migrating a system from one Foreman + Katello installation 6 or Capsule / to another in the same infrastructure 232 | 233 | In order to manually balance the load over multiple Capsule servers, you might 234 | want to move some existing systems to newly deployed Capsules. You can easily 235 | do this by running the bootstrap.py script like the examples below. Mind that 236 | you still have to manually revoke any Puppet certificates on the old capsules! 237 | 238 | ~~~ 239 | # ./bootstrap.py -l admin --new-capsule --server capsule.example.com 240 | ~~~ 241 | 242 | If you want to change the hostgroup and location of the system at the same 243 | time, run: 244 | 245 | ~~~ 246 | # ./bootstrap.py -l admin --new-capsule --server capsule.example.com \ 247 | --hostgroup mygroup --location mylocation 248 | ~~~ 249 | 250 | ### Enabling additional repositories at registration time. 251 | 252 | It is recommended to set which repositories that you want enabled on your activation keys via the UI or via `hammer activation-key product-content`. However, older versions of `subscription-manager` (versions < 1.10) do not support product content overrides. The `--enablerepos` switch accepts a comma separated lists of repositories that are passed to `subscription-manager` that will be enabled at registration time. 253 | 254 | ~~~ 255 | # ./bootstrap.py -l admin \ 256 | -s foreman.example.com \ 257 | -o "Red Hat" \ 258 | -L RDU \ 259 | -g "RHEL7/Crash" \ 260 | -a ak-Reg_To_Crash \ 261 | --enablerepos=rhel-7-server-extras-rpms,rhel-7-server-optional-rpms 262 | ~~~ 263 | 264 | ### Creating a domain for a host at registration time. 265 | 266 | To create a host record, the DNS domain of a host needs to exist (in Foreman) prior to running `bootstrap.py`. If the domain does not exist, it can be added via the `--add-domain` switch. 267 | 268 | ~~~ 269 | # hostname 270 | client.linux.example.com 271 | 272 | # ./bootstrap.py -l admin \ 273 | -s foreman.example.com \ 274 | -o "Red Hat" \ 275 | -L RDU \ 276 | -g "RHEL7/Crash" \ 277 | -a ak-Reg_To_Crash 278 | 279 | [NOTIFICATION], [2016-12-05 09:15:29], 280 | [Domain linux.example.com doesn't exist in Foreman, consider using the --add-domain option.] 281 | ~~~ 282 | 283 | Run the script again including the `--add-domain` option 284 | 285 | ~~~ 286 | #./bootstrap.py -l admin \ 287 | -s foreman.example.com \ 288 | -o "Red Hat" \ 289 | -L RDU \ 290 | -g "RHEL7/Crash" \ 291 | -a ak-Reg_To_Crash \ 292 | --add-domain 293 | 294 | [RUNNING], [2016-12-05 09:19:10], [Calling Foreman API to create domain 295 | linux.example.com associated with the org & location] 296 | [RUNNING], [2016-12-05 09:19:10], [Calling Foreman API to create a host entry 297 | associated with the group & org] 298 | [SUCCESS], [2016-12-05 09:19:10], [Successfully created host 299 | client.linux.example.com], completed successfully. 300 | ~~~ 301 | 302 | ### Enabling Remote Execution 303 | 304 | bootstrap.py now includes the `--rex` & `--rex-user` features which allow the administrator to deploy the required SSH keys. 305 | 306 | ~~~ 307 | 308 | # ./bootstrap.py -l admin \ 309 | -s foreman.example.com \ 310 | -o "Red Hat" \ 311 | -L RDU \ 312 | -g "RHEL7/Crash" \ 313 | -a ak-Reg_To_Crash \ 314 | --rex \ 315 | --rex-user root 316 | 317 | [SUCCESS], [2016-12-02 06:37:09], [/usr/bin/yum -y remove rhn-setup rhn-client-tools yum-rhn-plugin rhnsd rhn-check rhnlib spacewalk-abrt spacewalk-oscap osad 'rh-*-rhui-client'], completed successfully. 318 | 319 | [NOTIFICATION], [2016-12-02 06:37:09], [Foreman's SSH key was added to /root/.ssh/authorized_keys] 320 | Key was added successfully. 321 | ~~~ 322 | 323 | Check the **root** users authorized key file. 324 | 325 | ~~~ 326 | cat ~/.ssh/authorized_keys 327 | ssh-rsa AAAAB3Nz.... foreman-proxy@foreman.example.com 328 | ~~~ 329 | 330 | #### Fetching Remote Execution SSH keys from an URL 331 | 332 | ~~~ 333 | # ./bootstrap.py -l admin \ 334 | -s foreman.example.com \ 335 | -o "Red Hat" \ 336 | -L RDU \ 337 | -g "RHEL7/Crash" \ 338 | -a ak-Reg_To_Crash \ 339 | --rex \ 340 | --rex-urlkeyfile https://idm.example.com/users/root/keys 341 | ~~~ 342 | 343 | #### Fetching Remote Execution SSH keys from proxies 344 | 345 | ~~~ 346 | # ./bootstrap.py -l admin \ 347 | -s foreman.example.com \ 348 | -o "Red Hat" \ 349 | -L RDU \ 350 | -g "RHEL7/Crash" \ 351 | -a ak-Reg_To_Crash \ 352 | --rex \ 353 | --rex-proxies foreman.example.com,proxy01.example.com,proxy02.example.com 354 | ~~~ 355 | 356 | #### Deploying Remote Execution SSH keys to a non-default location 357 | 358 | ~~~ 359 | # ./bootstrap.py -l admin \ 360 | -s foreman.example.com \ 361 | -o "Red Hat" \ 362 | -L RDU \ 363 | -g "RHEL7/Crash" \ 364 | -a ak-Reg_To_Crash \ 365 | --rex \ 366 | --rex-user root \ 367 | --rex-authpath /etc/ssh/keys/root 368 | ~~~ 369 | 370 | ### Skipping particular steps: 371 | 372 | Sometimes, you may want to skip certain steps of the bootstrapping process. the `--skip` switch provides this. It currently has the following parameters 373 | 374 | * `foreman` - Skips any Foreman setup steps, please note that you MUST pass the Organization's LABEL, not NAME when using this. (equivalent to the deprecated `--skip-foreman` option) 375 | * `puppet` - Does not install puppet (equivalent to the deprecated `--skip-puppet` option) 376 | * `migration` - Skips RHN/Spacewalk registration detection. This option prevents `rhn-classic-migrate-to-rhsm` from timing out and failing on RHN/Spacewalk systems that aren't available. 377 | * `prereq-update` - Skips update of `yum`, `openssl` and `python` 378 | * `katello-agent` - Does not install the `katello-agent` package (DEPRECATED) 379 | * `katello-host-tools` - Does not install the `katello-host-tools` package 380 | * `remove-obsolete-packages` - Does not remove the Classic/RHN/Spacewalk/RHUI packages. (equivalent to `--no-remove-obsolete-packages`) 381 | * `puppet-enable` - Does not enable and start the puppet daemon on the client. 382 | 383 | **Note:** it is strongly preferred to use the `--skip` option in lieu of the individual `--skip-foreman`, `--skip-puppet`, and `--no-remove-obsolete-packages` options. 384 | 385 | ~~~ 386 | # ./bootstrap.py -l admin \ 387 | -s foreman.example.com \ 388 | -o "Red Hat" \ 389 | -L RDU \ 390 | -g "RHEL7/Crash" \ 391 | -a ak-Reg_To_Crash \ 392 | --skip prereq-update --skip migration 393 | ~~~ 394 | 395 | ### Providing an arbitrary Fully Qualified Domain Name. 396 | 397 | Many users have either hostnames that are short (`hostname -f` or python's `socket.getfqdn` returns a hostname that isn't an FQDN) or non-RFC compliant (containing a character such as an underscore `-` which fails Foreman's hostname validation. 398 | 399 | In many cases, the user cannot update his/her system to provide a FQDN. bootstrap.py provides the `--fqdn` which allows the user to specify the FQDN that will be reported to Foreman 400 | 401 | **Prerequisites** 402 | 403 | The user needs to set to **False** the `create_new_host_when_facts_are_uploaded` and ` create_new_host_when_report_is_uploaded` options. If these options are not set, a host entry will be created based upon the facts provided by facter. This can be done with hammer. 404 | 405 | ~~~ 406 | hammer settings set \ 407 | --name create_new_host_when_facts_are_uploaded \ 408 | --value false 409 | hammer settings set \ 410 | --name create_new_host_when_report_is_uploaded \ 411 | --value false 412 | ~~~ 413 | 414 | Example Usage 415 | ~~~ 416 | # hostname -f 417 | node-100 418 | 419 | # python -c 'import socket; print socket.getfqdn()' 420 | node-100 421 | 422 | # ./bootstrap.py -l admin \ 423 | -s foreman.example.com \ 424 | -o "Red Hat" \ 425 | -L RDU \ 426 | -g "RHEL7/Crash" \ 427 | -a ak-Reg_To_Crash \ 428 | --fqdn node-100.example.com 429 | ~~~ 430 | 431 | ### Changing the method bootstrap uses to download the katello-ca-consumer RPM 432 | 433 | By default, the bootstrap script uses HTTPS to download the `katello-ca-consumer` RPM. In some environments, it is desired to connect via HTTP. the `--download-method` option can be used to change the download method that bootstrap uses from HTTPS to HTTP. 434 | 435 | ~~~ 436 | ./bootstrap.py -l admin \ 437 | -s foreman.example.com \ 438 | -o "Red Hat" \ 439 | -L RDU \ 440 | -g "RHEL7/Crash" \ 441 | -a ak-Reg_To_Crash \ 442 | --download-method http 443 | ~~~ 444 | 445 | ### Providing the IP address to Foreman 446 | 447 | Foreman requires the IP address of the machine to perform remote execution or re-deploy the machine using kickstart. 448 | 449 | On machines with multiple interfaces or multiple addresses on one interface, it might be needed to override the auto-detection of the address and provide a specific address to Foreman. 450 | 451 | ~~~ 452 | ./bootstrap.py -l admin \ 453 | -s foreman.example.com \ 454 | -o "Red Hat" \ 455 | -L RDU \ 456 | -g "RHEL7/Crash" \ 457 | -a ak-Reg_To_Crash \ 458 | --ip 192.0.2.23 459 | ~~~ 460 | 461 | 462 | ### Configuring Puppet on the client to run only in noop mode 463 | 464 | When migrating or registering clients which may have never been managed via Puppet, it may be useful to configure the agent in `noop` mode. This allows the client to be managed via Foreman, while getting facts & reports about its configuration state, without making any changes to it. The `--puppet-noop` switch facilitates this behavior. 465 | 466 | ~~~ 467 | ./bootstrap.py -l admin \ 468 | -s foreman.example.com \ 469 | -o "Red Hat" \ 470 | -L RDU \ 471 | -g "RHEL7/Crash" \ 472 | -a ak-Reg_To_Crash \ 473 | --puppet-noop 474 | ~~~ 475 | 476 | ### Providing a repository with the subscription-manager packages 477 | 478 | For clients who do not have subscription-manager installed (which is a prerequisite of `bootstrap.py`), the `deps-repository-url` option can be used to specify a yum repository which contains the `subscription-manager` RPMs 479 | On your Foreman instance, kickstart repositories are available via HTTP, and are ideal to be used in this scenario. However, any yum repository with the required packages would work. 480 | 481 | ~~~ 482 | ./bootstrap.py -l admin \ 483 | -s foreman.example.com \ 484 | -o "Red Hat" \ 485 | -L RDU \ 486 | -g "RHEL7/Crash" \ 487 | -a ak-Reg_To_Crash \ 488 | --download-method https \ 489 | --deps-repository-url "http://server.example.com/pulp/repos/Example/Library/content/dist/rhel/server/7/7.2/x86_64/kickstart/" 490 | ~~~ 491 | 492 | Also, the `--deps-repository-gpg-key` option (defaults to `file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release`) is available if the GPG key for the repository differs from `/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release` 493 | 494 | ### Installing user specified packages 495 | 496 | For some users who do not have a configuration management or automation solution, `bootstrap.py` provides a means to install user specified packages via the `--install-packages` switch. 497 | 498 | ~~~ 499 | ./bootstrap.py -l admin \ 500 | -s foreman.example.com \ 501 | -o "Red Hat" \ 502 | -L RDU \ 503 | -g "RHEL7/Crash" \ 504 | -a ak-Reg_To_Crash \ 505 | --install-packages csh,dstat 506 | 507 | ~~~ 508 | 509 | ### Changing the API/Subscription Manager timeouts 510 | 511 | On busy servers, it is sometimes useful to increase the amount of time that the system waits before timing out during registration and subscription tasks. 512 | `bootstrap.py` defaults to an timeout of **900** seconds for APIs. Additionally, the `server_timeout` parameter for `subscription-manager` 513 | is configured with this value. If desired, this value can be overridden using the `--timeout` option. 514 | 515 | ~~~ 516 | ./bootstrap.py -l admin \ 517 | -s foreman.example.com \ 518 | -o "Red Hat" \ 519 | -L RDU \ 520 | -g "RHEL7/Crash" \ 521 | -a ak-Reg_To_Crash \ 522 | --timeout 1800 523 | 524 | ~~~ 525 | 526 | ### Using an alternative Puppet master or Puppet CA 527 | 528 | When attaching a client to a setup, where Puppet runs outside of the Foreman setup, you can configure the Puppet agent to use an alternative Puppet master using the `--puppet-server` switch. 529 | 530 | ~~~ 531 | ./bootstrap.py -l admin \ 532 | -s foreman.example.com \ 533 | -o "Red Hat" \ 534 | -L RDU \ 535 | -g "RHEL7/Crash" \ 536 | -a ak-Reg_To_Crash \ 537 | --puppet-server=puppet.example.com 538 | ~~~ 539 | 540 | In the case the Puppet CA is running on a different server, you can use the `--puppet-ca-server` switch for the server hostname and the `--puppet-ca-port` one for the port. 541 | 542 | ~~~ 543 | ./bootstrap.py -l admin \ 544 | -s foreman.example.com \ 545 | -o "Red Hat" \ 546 | -L RDU \ 547 | -g "RHEL7/Crash" \ 548 | -a ak-Reg_To_Crash \ 549 | --puppet-server=puppet.example.com \ 550 | --puppet-ca-server=puppetca.example.com 551 | ~~~ 552 | 553 | ~~~ 554 | ./bootstrap.py -l admin \ 555 | -s foreman.example.com \ 556 | -o "Red Hat" \ 557 | -L RDU \ 558 | -g "RHEL7/Crash" \ 559 | -a ak-Reg_To_Crash \ 560 | --puppet-server=puppet.example.com \ 561 | --puppet-ca-port=8141 562 | ~~~ 563 | 564 | ### Adding a comment when registering a node. 565 | 566 | When registering a client, it is sometimes desirable to add a comment, denoting internal information such as the owner of the server or other site-specific info. This can be accomplished with the `--comment` option. 567 | 568 | ~~~ 569 | ./bootstrap.py -l admin \ 570 | -s foreman.example.com \ 571 | -o "Red Hat" \ 572 | -L RDU \ 573 | -g "RHEL7/Crash" \ 574 | -a ak-Reg_To_Crash \ 575 | --comment 'Crash Testing Server' 576 | ~~~ 577 | 578 | ### Ignoring Registration Failures 579 | 580 | When registering a client, it is sometimes desired to ignore registration failures reported via `subscription-manager` or `rhn-migrate-classic-to-rhsm`. The `--ignore-registration-failures` option allows `bootstrap.py` to continue running even when these commands return a non-zero error code. **NOTE**: it is the responsibility of the end-user to ensure, when using this option, that registration has completed successfully. 581 | 582 | ~~~ 583 | ./bootstrap.py -l admin \ 584 | -s foreman.example.com \ 585 | -o "Red Hat" \ 586 | -L RDU \ 587 | -g "RHEL7/Crash" \ 588 | -a ak-Reg_To_Crash \ 589 | --ignore-registration-failures 590 | ~~~ 591 | 592 | ### Preserving RHSM Proxy Settings 593 | 594 | When moving clients from RHSM to Katello or a different RHSM provider, the proxy settings in `/etc/rhsm/rhsm.conf` might get lost. Using `--preserve-rhsm-proxy` you can ensure that the old settings will be restored for the new configuration. 595 | 596 | 597 | ~~~ 598 | ./bootstrap.py -l admin \ 599 | -s foreman.example.com \ 600 | -o "Red Hat" \ 601 | -L RDU \ 602 | -g "RHEL7/Crash" \ 603 | -a ak-Reg_To_Crash \ 604 | --preserve-rhsm-proxy 605 | ~~~ 606 | 607 | ### Installing the Katello agent 608 | 609 | Bootstrap no longer defaults to installing the `katello-agent` package. The recommended default is to install the `katello-host-tools` package. If it is desired to install the `katello-agent` package, pass `--install-katello-agent` as a parameter. 610 | 611 | 612 | ~~~ 613 | ./bootstrap.py -l admin \ 614 | -s foreman.example.com \ 615 | -o "Red Hat" \ 616 | -L RDU \ 617 | -g "RHEL7/Crash" \ 618 | -a ak-Reg_To_Crash \ 619 | --install-katello-agent 620 | ~~~ 621 | 622 | # Help / Available options: 623 | 624 | ~~~ 625 | Foreman Bootstrap Script 626 | This script is designed to register new systems or to migrate an existing system to a Foreman server with Katello 627 | Usage: bootstrap.py -l admin -s foreman.example.com -o 'Default Organization' -L 'Default Location' -g My_Hostgroup -a My_Activation_Key 628 | 629 | Options: 630 | --version show program's version number and exit 631 | -h, --help show this help message and exit 632 | -s foreman_fqdn, --server=foreman_fqdn 633 | FQDN of Foreman OR Capsule - omit https:// 634 | -l LOGIN, --login=LOGIN 635 | Login user for API Calls 636 | -p PASSWORD, --password=PASSWORD 637 | Password for specified user. Will prompt if omitted 638 | --fqdn=FQDN Set an explicit FQDN, overriding detected FQDN from 639 | socket.getfqdn(), currently detected as 640 | client.example.com 641 | --legacy-login=LOGIN Login user for Satellite 5 API Calls 642 | --legacy-password=PASSWORD 643 | Password for specified Satellite 5 user. Will prompt 644 | if omitted 645 | --legacy-purge Purge system from the Legacy environment (e.g. Sat5) 646 | -a ACTIVATIONKEY, --activationkey=ACTIVATIONKEY 647 | Activation Key to register the system 648 | -P, --skip-puppet Do not install Puppet 649 | --skip-foreman Do not create a Foreman host. Implies --skip-puppet. 650 | When using --skip-foreman, you MUST pass the 651 | Organization's LABEL, not NAME 652 | --force-content-source 653 | Force the content source to be the registration 654 | capsule (it overrides the value in the host group if 655 | any is defined) 656 | --content-only Setup host for content only. Alias to --skip foreman. 657 | Implies --skip-puppet. When using --content-only, you 658 | MUST pass the Organization's LABEL, not NAME 659 | -g HOSTGROUP, --hostgroup=HOSTGROUP 660 | Title of the Hostgroup in Foreman that the host is to 661 | be associated with 662 | -L LOCATION, --location=LOCATION 663 | Title of the Location in Foreman that the host is to 664 | be associated with 665 | -O OPERATINGSYSTEM, --operatingsystem=OPERATINGSYSTEM 666 | Title of the Operating System in Foreman that the host 667 | is to be associated with 668 | --partitiontable=PARTITIONTABLE 669 | Name of the Partition Table in Foreman that the host 670 | is to be associated with 671 | -o ORG, --organization=ORG 672 | Name of the Organization in Foreman that the host is 673 | to be associated with 674 | -S ARGS, --subscription-manager-args=ARGS 675 | Which additional arguments shall be passed to 676 | subscription-manager 677 | --rhn-migrate-args=ARGS 678 | Which additional arguments shall be passed to rhn- 679 | migrate-classic-to-rhsm 680 | -u, --update Fully Updates the System 681 | -v, --verbose Verbose output 682 | -f, --force Force registration (will erase old katello and puppet 683 | certs) 684 | --add-domain Automatically add the clients domain to Foreman 685 | --puppet-noop Configure Puppet agent to only run in noop mode 686 | --puppet-server=PUPPET_SERVER 687 | Configure Puppet agent to use this server as master 688 | (defaults to the Foreman server) 689 | --puppet-ca-server=PUPPET_CA_SERVER 690 | Configure Puppet agent to use this server as CA 691 | (defaults to the Foreman server) 692 | --puppet-ca-port=PUPPET_CA_PORT 693 | Configure Puppet agent to use this port to connect to 694 | the CA 695 | --remove Instead of registering the machine to Foreman remove 696 | it 697 | -r RELEASE, --release=RELEASE 698 | Specify release version 699 | -R, --remove-obsolete-packages 700 | Remove old Red Hat Network and RHUI Packages (default) 701 | --download-method=DOWNLOADMETHOD 702 | Method to download katello-ca-consumer package (e.g. 703 | http or https) 704 | --no-remove-obsolete-packages 705 | Don't remove old Red Hat Network and RHUI Packages 706 | --unmanaged Add the server as unmanaged. Useful to skip 707 | provisioning dependencies. 708 | --rex Install Foreman's SSH key for remote execution. 709 | --rex-user=REMOTE_EXEC_USER 710 | Local user used by Foreman's remote execution feature. 711 | --rex-proxies=REMOTE_EXEC_PROXIES 712 | Comma separated list of proxies to install Foreman's 713 | SSH keys for remote execution. 714 | --rex-urlkeyfile=REMOTE_EXEC_URL 715 | HTTP/S location of a file containing one or 716 | more Foreman's SSH keys for remote execution. 717 | --rex-apikeys Fetch Foreman's SSH keys from the API. 718 | --rex-authpath=REMOTE_EXEC_AUTHPATH 719 | Full path to local authorized_keys file in order to 720 | install Foreman's SSH keys for remote execution. 721 | Default ~/.ssh/authorized_keys 722 | --enablerepos=enablerepos 723 | Repositories to be enabled via subscription-manager - 724 | comma separated 725 | --skip=SKIP Skip the listed steps (choices: ['foreman', 'puppet', 726 | 'migration', 'prereq-update', 'katello-agent', 727 | 'remove-obsolete-packages', 'puppet-enable', 'katello-host-tools']) 728 | --ip=IP IPv4 address of the primary interface in Foreman 729 | (defaults to the address used to make request to 730 | Foreman) 731 | --deps-repository-url=DEPS_REPOSITORY_URL 732 | URL to a repository that contains the subscription- 733 | manager RPMs 734 | --deps-repository-gpg-key=DEPS_REPOSITORY_GPG_KEY 735 | GPG Key to the repository that contains the 736 | subscription-manager RPMs 737 | --install-packages=installpackages 738 | List of packages to be additionally installed - comma 739 | separated 740 | --new-capsule Switch the server to a new capsule for content and 741 | Puppet. Pass --server with the Capsule FQDN as well. 742 | -t timeout, --timeout=timeout 743 | Timeout (in seconds) for API calls and subscription- 744 | manager registration. Defaults to 900 745 | -c COMMENT, --comment=COMMENT 746 | Add a host comment 747 | --ignore-registration-failures 748 | Continue running even if registration via 749 | subscription-manager/rhn-migrate-classic-to-rhsm 750 | returns a non-zero return code. 751 | --preserve-rhsm-proxy 752 | Preserve proxy settings in /etc/rhsm/rhsm.conf when 753 | migrating RHSM -> RHSM 754 | --install-katello-agent 755 | Installs the Katello Agent 756 | ~~~ 757 | 758 | # Additional Notes 759 | 760 | ## FIPS support 761 | 762 | On systems with FIPS enabled (where `/proc/sys/crypto/fips_enabled == 1`), algorithms such as MD5 are disallowed. Bootstrap will configure `digest_algorithm = sha256` in puppet.conf to allow successful puppet runs. However, the signing algorithm **must** match on the Puppet Master. It is expected that the Puppet Masters are configured with the **same** algorithm prior to running `bootstrap.py` on the clients. 763 | 764 | # Ansible integration 765 | 766 | The `bootstrap.yml` file contains a playbook for [Ansible](https://www.ansible.com/) which can be used to copy `bootstrap.py` to the target machine and execute it there with predefined parameters. 767 | 768 | # For developers and contributors: 769 | 770 | See [CONTRIBUTING.md](https://github.com/Katello/katello-client-bootstrap/blob/master/CONTRIBUTING.md) 771 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Script to register a new host to Foreman/Satellite 4 | or move it from Satellite 5 to 6. 5 | 6 | Use `pydoc ./bootstrap.py` to get the documentation. 7 | Use `awk -F'# >' 'NF>1 {print $2}' ./bootstrap.py` to see the flow. 8 | Use `/usr/libexec/platform-python bootstrap.py` on RHEL8 9 | """ 10 | 11 | import getpass 12 | try: 13 | # pylint:disable=invalid-name 14 | import urllib2 15 | from urllib import urlencode 16 | urllib_urlopen = urllib2.urlopen 17 | urllib_urlerror = urllib2.URLError 18 | urllib_httperror = urllib2.HTTPError 19 | urllib_basehandler = urllib2.BaseHandler 20 | urllib_request = urllib2.Request 21 | urllib_build_opener = urllib2.build_opener 22 | urllib_install_opener = urllib2.install_opener 23 | except ImportError: 24 | # pylint:disable=invalid-name,no-member 25 | import urllib 26 | import urllib.request 27 | import urllib.error 28 | from urllib.parse import urlencode 29 | urllib_urlopen = urllib.request.urlopen 30 | urllib_urlerror = urllib.error.URLError 31 | urllib_httperror = urllib.error.HTTPError 32 | urllib_basehandler = urllib.request.BaseHandler 33 | urllib_request = urllib.request.Request 34 | urllib_build_opener = urllib.request.build_opener 35 | urllib_install_opener = urllib.request.install_opener 36 | import base64 37 | import sys 38 | try: 39 | from commands import getstatusoutput 40 | NEED_STATUS_SHIFT = True 41 | except ImportError: 42 | from subprocess import getstatusoutput 43 | NEED_STATUS_SHIFT = False 44 | import platform 45 | import socket 46 | import os 47 | import pwd 48 | import glob 49 | import shutil 50 | import tempfile 51 | from datetime import datetime 52 | from optparse import OptionParser # pylint:disable=deprecated-module 53 | try: 54 | from ConfigParser import SafeConfigParser 55 | except ImportError: 56 | from configparser import ConfigParser as SafeConfigParser 57 | try: 58 | import yum # pylint:disable=import-error 59 | USE_YUM = True 60 | except ImportError: 61 | import dnf # pylint:disable=import-error 62 | USE_YUM = False 63 | import rpm # pylint:disable=import-error 64 | 65 | 66 | VERSION = '1.7.9' 67 | 68 | # Python 2.4 only supports octal numbers by prefixing '0' 69 | # Python 3 only support octal numbers by prefixing '0o' 70 | # Therefore use the decimal notation for file permissions 71 | OWNER_ONLY_DIR = 448 # octal: 700 72 | OWNER_ONLY_FILE = 384 # octal: 600 73 | 74 | 75 | def get_architecture(): 76 | """ 77 | Helper function to get the architecture x86_64 vs. x86. 78 | """ 79 | return os.uname()[4] 80 | 81 | 82 | ERROR_COLORS = { 83 | """Colors to be used by the multiple `print_*` functions.""" 84 | 'HEADER': '\033[95m', 85 | 'OKBLUE': '\033[94m', 86 | 'OKGREEN': '\033[92m', 87 | 'WARNING': '\033[93m', 88 | 'FAIL': '\033[91m', 89 | 'ENDC': '\033[0m', 90 | } 91 | 92 | SUBSCRIPTION_MANAGER_SERVER_TIMEOUT_VERSION = '1.18.2' 93 | SUBSCRIPTION_MANAGER_MIGRATION_MINIMAL_VERSION = '1.14.2' 94 | SUBSCRIPTION_MANAGER_MIGRATION_REMOVE_PKGS_VERSION = '1.18.2' 95 | 96 | 97 | def filter_string(string): 98 | """Helper function to filter out passwords from strings""" 99 | if options.password: 100 | string = string.replace(options.password, '******') 101 | if options.legacy_password: 102 | string = string.replace(options.legacy_password, '******') 103 | return string 104 | 105 | 106 | def print_error(msg): 107 | """Helper function to output an ERROR message.""" 108 | print_message(color_string('ERROR', 'FAIL'), 'EXITING: [%s] failed to execute properly.' % msg) 109 | 110 | 111 | def print_warning(msg): 112 | """Helper function to output a WARNING message.""" 113 | print_message(color_string('WARNING', 'WARNING'), 'NON-FATAL: [%s] failed to execute properly.' % msg) 114 | 115 | 116 | def print_success(msg): 117 | """Helper function to output a SUCCESS message.""" 118 | print_message(color_string('SUCCESS', 'OKGREEN'), '[%s], completed successfully.' % msg) 119 | 120 | 121 | def print_running(msg): 122 | """Helper function to output a RUNNING message.""" 123 | print_message(color_string('RUNNING', 'OKBLUE'), '[%s]' % msg) 124 | 125 | 126 | def print_generic(msg): 127 | """Helper function to output a NOTIFICATION message.""" 128 | print_message('NOTIFICATION', '[%s]' % msg) 129 | 130 | 131 | def print_message(prefix, msg): 132 | """Helper function to output a message with a prefix""" 133 | print("[%s], [%s], %s" % (prefix, datetime.now().strftime('%Y-%m-%d %H:%M:%S'), msg)) 134 | 135 | 136 | def color_string(msg, color): 137 | """Helper function to add ANSII colors to a message""" 138 | return '%s%s%s' % (ERROR_COLORS[color], msg, ERROR_COLORS['ENDC']) 139 | 140 | 141 | def exec_failok(command): 142 | """Helper function to call a command with only warning if failing.""" 143 | return exec_command(command, True) 144 | 145 | 146 | def exec_failexit(command): 147 | """Helper function to call a command with error and exit if failing.""" 148 | return exec_command(command, False) 149 | 150 | 151 | def exec_command(command, failok=False): 152 | """Helper function to call a command and handle errors and output.""" 153 | filtered_command = filter_string(command) 154 | print_running(filtered_command) 155 | retcode, output = getstatusoutput(command) 156 | if NEED_STATUS_SHIFT: 157 | retcode = os.WEXITSTATUS(retcode) 158 | print(output) 159 | if retcode != 0: 160 | if failok: 161 | print_warning(filtered_command) 162 | else: 163 | print_error(filtered_command) 164 | sys.exit(retcode) 165 | else: 166 | print_success(filtered_command) 167 | return retcode 168 | 169 | 170 | def delete_file(filename): 171 | """Helper function to delete files.""" 172 | if not os.path.exists(filename): 173 | print_generic("%s does not exist - not removing" % filename) 174 | return 175 | try: 176 | os.remove(filename) 177 | print_success("Removing %s" % filename) 178 | except OSError: 179 | exception = sys.exc_info()[1] 180 | print_generic("Error when removing %s - %s" % (filename, exception.strerror)) 181 | print_error("Removing %s" % filename) 182 | sys.exit(1) 183 | 184 | 185 | def delete_directory(directoryname): 186 | """Helper function to delete directories.""" 187 | if not os.path.exists(directoryname): 188 | print_generic("%s does not exist - not removing" % directoryname) 189 | return 190 | try: 191 | shutil.rmtree(directoryname) 192 | print_success("Removing %s" % directoryname) 193 | except OSError: 194 | exception = sys.exc_info()[1] 195 | print_generic("Error when removing %s - %s" % (directoryname, exception.strerror)) 196 | print_error("Removing %s" % directoryname) 197 | sys.exit(1) 198 | 199 | 200 | def call_yum(command, params="", failonerror=True): 201 | """ 202 | Helper function to call a yum command on a list of packages. 203 | pass failonerror = False to make yum commands non-fatal 204 | """ 205 | exec_command("/usr/bin/yum -y %s %s" % (command, params), not failonerror) 206 | 207 | 208 | def check_migration_version(required_version): 209 | """ 210 | Verify that the command 'subscription-manager-migration' isn't too old. 211 | """ 212 | status, err = check_package_version('subscription-manager-migration', required_version) 213 | 214 | return (status, err) 215 | 216 | 217 | def check_subman_version(required_version): 218 | """ 219 | Verify that the command 'subscription-manager' isn't too old. 220 | """ 221 | status, _ = check_package_version('subscription-manager', required_version) 222 | 223 | return status 224 | 225 | 226 | def check_package_version(package_name, package_version): 227 | """ 228 | Verify that the version of a package 229 | """ 230 | required_version = ('0', package_version, '1') 231 | err = "%s not found" % package_name 232 | 233 | transaction_set = rpm.TransactionSet() 234 | db_result = transaction_set.dbMatch('name', package_name) 235 | for package in db_result: 236 | try: 237 | p_name = package['name'].decode('ascii') 238 | p_version = package['version'].decode('ascii') 239 | except AttributeError: 240 | p_name = package['name'] 241 | p_version = package['version'] 242 | if rpm.labelCompare(('0', p_version, '1'), required_version) < 0: 243 | err = "%s %s is too old" % (p_name, p_version) 244 | else: 245 | err = None 246 | 247 | return (err is None, err) 248 | 249 | 250 | def setup_yum_repo(url, gpg_key): 251 | """ 252 | Configures a yum repository at /etc/yum.repos.d/katello-client-bootstrap-deps.repo 253 | """ 254 | submanrepoconfig = SafeConfigParser() 255 | submanrepoconfig.add_section('kt-bootstrap') 256 | submanrepoconfig.set('kt-bootstrap', 'name', 'Subscription-manager dependencies for katello-client-bootstrap') 257 | submanrepoconfig.set('kt-bootstrap', 'baseurl', url) 258 | submanrepoconfig.set('kt-bootstrap', 'enabled', '1') 259 | submanrepoconfig.set('kt-bootstrap', 'gpgcheck', '1') 260 | submanrepoconfig.set('kt-bootstrap', 'gpgkey', gpg_key) 261 | submanrepoconfig.write(open('/etc/yum.repos.d/katello-client-bootstrap-deps.repo', 'w')) 262 | print_generic('Building yum metadata cache. This may take a few minutes') 263 | call_yum('makecache') 264 | 265 | 266 | def install_prereqs(): 267 | """ 268 | Install subscription manager and its prerequisites. 269 | If subscription-manager is installed already, check to see if we are migrating. If yes, install subscription-manager-migration. 270 | Else if subscription-manager and subscription-manager-migration are available in a configured repo, install them both. 271 | Otherwise, exit and inform user that we cannot proceed 272 | """ 273 | print_generic("Checking subscription manager prerequisites") 274 | if options.deps_repository_url: 275 | print_generic("Enabling %s as a repository for dependency RPMs" % options.deps_repository_url) 276 | setup_yum_repo(options.deps_repository_url, options.deps_repository_gpg_key) 277 | if USE_YUM: 278 | yum_base = yum.YumBase() 279 | pkg_list = yum_base.doPackageLists(patterns=['subscription-manager']) 280 | subman_installed = pkg_list.installed 281 | subman_available = pkg_list.available 282 | else: 283 | dnf_base = dnf.Base() 284 | dnf_base.conf.read() 285 | dnf_base.conf.substitutions.update_from_etc('/') 286 | dnf_base.read_all_repos() 287 | try: 288 | dnf_base.fill_sack() 289 | except: # noqa: E722, pylint:disable=bare-except 290 | pass 291 | pkg_list = dnf_base.sack.query().filter(name='subscription-manager') 292 | subman_installed = pkg_list.installed().run() 293 | subman_available = pkg_list.available().run() 294 | call_yum("remove", "subscription-manager-gnome", False) 295 | if subman_installed: 296 | if check_rhn_registration() and 'migration' not in options.skip: 297 | print_generic("installing subscription-manager-migration") 298 | call_yum("install", "'subscription-manager-migration-*'", False) 299 | print_generic("subscription-manager is installed already. Attempting update") 300 | call_yum("update", "subscription-manager", False) 301 | call_yum("update", "'subscription-manager-migration-*'", False) 302 | elif subman_available: 303 | print_generic("subscription-manager NOT installed. Installing.") 304 | call_yum("install", "subscription-manager") 305 | call_yum("install", "'subscription-manager-migration-*'", False) 306 | else: 307 | print_error("Cannot find subscription-manager in any configured repository. Consider using the --deps-repository-url switch to specify a repository with the subscription-manager RPMs") 308 | sys.exit(1) 309 | if 'prereq-update' not in options.skip: 310 | call_yum("update", "yum openssl python", False) 311 | if options.deps_repository_url: 312 | delete_file('/etc/yum.repos.d/katello-client-bootstrap-deps.repo') 313 | 314 | 315 | def is_fips(): 316 | """ 317 | Checks to see if the system is FIPS enabled. 318 | """ 319 | try: 320 | fips_file = open("/proc/sys/crypto/fips_enabled", "r") 321 | fips_status = fips_file.read(1) 322 | except IOError: 323 | fips_status = "0" 324 | return fips_status == "1" 325 | 326 | 327 | def get_rhsm_proxy(): 328 | """ 329 | Return the proxy server settings from /etc/rhsm/rhsm.conf as dictionary proxy_config. 330 | """ 331 | rhsmconfig = SafeConfigParser() 332 | rhsmconfig.read('/etc/rhsm/rhsm.conf') 333 | proxy_options = [option for option in rhsmconfig.options('server') if option.startswith('proxy')] 334 | proxy_config = {} 335 | for option in proxy_options: 336 | proxy_config[option] = rhsmconfig.get('server', option) 337 | return proxy_config 338 | 339 | 340 | def set_rhsm_proxy(proxy_config): 341 | """ 342 | Set proxy server settings in /etc/rhsm/rhsm.conf from dictionary saved_proxy_config. 343 | """ 344 | rhsmconfig = SafeConfigParser() 345 | rhsmconfig.read('/etc/rhsm/rhsm.conf') 346 | for option in proxy_config.keys(): 347 | rhsmconfig.set('server', option, proxy_config[option]) 348 | rhsmconfig.write(open('/etc/rhsm/rhsm.conf', 'w')) 349 | 350 | 351 | def get_bootstrap_rpm(clean=False, unreg=True): 352 | """ 353 | Retrieve Client CA Certificate RPMs from the Satellite 6 server. 354 | Uses --insecure options to curl(1) if instructed to download via HTTPS 355 | This function is usually called with clean=options.force, which ensures 356 | clean_katello_agent() is called if --force is specified. You can optionally 357 | pass unreg=False to bypass unregistering a system (e.g. when moving between 358 | capsules. 359 | """ 360 | if clean: 361 | clean_katello_agent() 362 | if os.path.exists('/etc/rhsm/ca/katello-server-ca.pem'): 363 | print_generic("A Katello CA certificate is already installed. Assuming system is registered.") 364 | print_generic("If you want to move the system to a different Content Proxy in the same setup, please use --new-capsule.") 365 | print_generic("If you want to remove the old host record and all data associated with it, please use --force.") 366 | print_generic("Exiting.") 367 | sys.exit(1) 368 | if os.path.exists('/etc/pki/consumer/cert.pem') and unreg: 369 | print_generic('System appears to be registered via another entitlement server. Attempting unregister') 370 | unregister_system() 371 | if options.download_method == "https": 372 | print_generic("Writing custom cURL configuration to allow download via HTTPS without certificate verification") 373 | curl_config_dir = tempfile.mkdtemp() 374 | curl_config = open(os.path.join(curl_config_dir, '.curlrc'), 'w') 375 | curl_config.write("insecure") 376 | curl_config.close() 377 | os.environ["CURL_HOME"] = curl_config_dir 378 | print_generic("Retrieving Client CA Certificate RPMs") 379 | exec_failexit("rpm -Uvh https://%s/pub/katello-ca-consumer-latest.noarch.rpm" % options.foreman_fqdn) 380 | print_generic("Deleting cURL configuration") 381 | delete_directory(curl_config_dir) 382 | os.environ.pop("CURL_HOME", None) 383 | else: 384 | print_generic("Retrieving Client CA Certificate RPMs") 385 | exec_failexit("rpm -Uvh http://%s/pub/katello-ca-consumer-latest.noarch.rpm" % options.foreman_fqdn) 386 | 387 | 388 | def disable_rhn_plugin(): 389 | """ 390 | Disable the RHN plugin for Yum 391 | """ 392 | if os.path.exists('/etc/yum/pluginconf.d/rhnplugin.conf'): 393 | rhnpluginconfig = SafeConfigParser() 394 | rhnpluginconfig.read('/etc/yum/pluginconf.d/rhnplugin.conf') 395 | if rhnpluginconfig.get('main', 'enabled') == '1': 396 | print_generic("RHN yum plugin was enabled. Disabling...") 397 | rhnpluginconfig.set('main', 'enabled', '0') 398 | rhnpluginconfig.write(open('/etc/yum/pluginconf.d/rhnplugin.conf', 'w')) 399 | if os.path.exists('/etc/sysconfig/rhn/systemid'): 400 | os.rename('/etc/sysconfig/rhn/systemid', '/etc/sysconfig/rhn/systemid.bootstrap-bak') 401 | 402 | 403 | def enable_rhsmcertd(): 404 | """ 405 | Enable and restart the rhsmcertd service 406 | """ 407 | enable_service("rhsmcertd") 408 | exec_service("rhsmcertd", "restart") 409 | 410 | 411 | def is_registered(): 412 | """ 413 | Check if all required certificates are in place (i.e. a system is 414 | registered to begin with) before we start changing things 415 | """ 416 | return (os.path.exists('/etc/rhsm/ca/katello-server-ca.pem') and 417 | os.path.exists('/etc/pki/consumer/cert.pem')) 418 | 419 | 420 | def migrate_systems(org_name, activationkey): 421 | """ 422 | Call `rhn-migrate-classic-to-rhsm` to migrate the machine from Satellite 423 | 5 to 6 using the organization name/label and the given activation key, and 424 | configure subscription manager with the baseurl of Satellite6's pulp. 425 | This allows the administrator to override the URL provided in the 426 | katello-ca-consumer-latest RPM, which is useful in scenarios where the 427 | Capsules/Servers are load-balanced or using subjectAltName certificates. 428 | If called with "--legacy-purge", uses "legacy-user" and "legacy-password" 429 | to remove the machine. 430 | Option "--force" is always passed so that `rhn-migrate-classic-to-rhsm` 431 | does not fail on channels which cannot be mapped either because they 432 | are cloned channels, custom channels, or do not exist in the destination. 433 | """ 434 | if 'foreman' in options.skip: 435 | org_label = org_name 436 | else: 437 | org_label = return_matching_katello_key('organizations', 'name="%s"' % org_name, 'label', False) 438 | print_generic("Calling rhn-migrate-classic-to-rhsm") 439 | options.rhsmargs += " --force --destination-url=https://%s:%s/rhsm" % (options.foreman_fqdn, RHSM_PORT) 440 | if options.legacy_purge: 441 | options.rhsmargs += " --legacy-user '%s' --legacy-password '%s'" % (options.legacy_login, options.legacy_password) 442 | if options.removepkgs and check_migration_version(SUBSCRIPTION_MANAGER_MIGRATION_REMOVE_PKGS_VERSION)[0]: 443 | options.rhsmargs += " --remove-rhn-packages" 444 | else: 445 | options.rhsmargs += " --keep" 446 | if check_subman_version(SUBSCRIPTION_MANAGER_SERVER_TIMEOUT_VERSION): 447 | exec_failok("/usr/sbin/subscription-manager config --server.server_timeout=%s" % options.timeout) 448 | exec_command("/usr/sbin/rhn-migrate-classic-to-rhsm --org %s --activation-key '%s' %s" % (org_label, activationkey, options.rhsmargs), options.ignore_registration_failures) 449 | exec_command("subscription-manager config --rhsm.baseurl=https://%s/pulp/repos" % options.foreman_fqdn, options.ignore_registration_failures) 450 | if options.release: 451 | exec_failexit("subscription-manager release --set %s" % options.release) 452 | enable_rhsmcertd() 453 | 454 | # When rhn-migrate-classic-to-rhsm is called with --keep, it will leave the systemid 455 | # file intact, which might confuse the (not yet removed) yum-rhn-plugin. 456 | # Move the file to a backup name & disable the RHN plugin, so the user can still restore it if needed. 457 | disable_rhn_plugin() 458 | 459 | 460 | def register_systems(org_name, activationkey): 461 | """ 462 | Register the host to Satellite 6's organization using 463 | `subscription-manager` and the given activation key. 464 | Option "--force" is given further. 465 | """ 466 | if 'foreman' in options.skip: 467 | org_label = org_name 468 | else: 469 | org_label = return_matching_katello_key('organizations', 'name="%s"' % org_name, 'label', False) 470 | print_generic("Calling subscription-manager") 471 | options.smargs += " --serverurl=https://%s:%s/rhsm --baseurl=https://%s/pulp/repos" % (options.foreman_fqdn, RHSM_PORT, options.foreman_fqdn) 472 | if options.force: 473 | options.smargs += " --force" 474 | if options.release: 475 | options.smargs += " --release %s" % options.release 476 | if check_subman_version(SUBSCRIPTION_MANAGER_SERVER_TIMEOUT_VERSION): 477 | exec_failok("/usr/sbin/subscription-manager config --server.server_timeout=%s" % options.timeout) 478 | exec_command("/usr/sbin/subscription-manager register --org '%s' --name '%s' --activationkey '%s' %s" % (org_label, FQDN, activationkey, options.smargs), options.ignore_registration_failures) 479 | enable_rhsmcertd() 480 | 481 | 482 | def unregister_system(): 483 | """Unregister the host using `subscription-manager`.""" 484 | print_generic("Cleaning old yum metadata") 485 | call_yum("clean", "metadata dbcache", False) 486 | print_generic("Unregistering") 487 | exec_failok("/usr/sbin/subscription-manager unregister") 488 | exec_failok("/usr/sbin/subscription-manager clean") 489 | 490 | 491 | def clean_katello_agent(): 492 | """Remove old Katello agent (aka Gofer) and certificate RPMs.""" 493 | print_generic("Removing old Katello agent and certs") 494 | call_yum("remove", "'katello-ca-consumer-*' katello-agent gofer katello-host-tools katello-host-tools-fact-plugin", False) 495 | delete_file("/etc/rhsm/ca/katello-server-ca.pem") 496 | 497 | 498 | def install_katello_agent(): 499 | """Install Katello agent (aka Gofer) and activate /start it.""" 500 | print_generic("Installing the Katello agent") 501 | call_yum("install", "katello-agent") 502 | enable_service("goferd") 503 | exec_service("goferd", "restart") 504 | 505 | 506 | def install_katello_host_tools(): 507 | """Install Katello Host Tools""" 508 | print_generic("Installing the Katello Host Tools") 509 | call_yum("install", "katello-host-tools") 510 | 511 | 512 | def clean_puppet(): 513 | """Remove old Puppet Agent and its configuration""" 514 | print_generic("Cleaning old Puppet Agent") 515 | call_yum("remove", "puppet-agent", False) 516 | delete_directory("/var/lib/puppet/") 517 | delete_directory("/opt/puppetlabs/puppet/cache") 518 | delete_directory("/etc/puppetlabs/puppet/ssl") 519 | 520 | 521 | def clean_environment(): 522 | """ 523 | Undefine `GEM_PATH`, `LD_LIBRARY_PATH` and `LD_PRELOAD` as many environments 524 | have it defined non-sensibly. 525 | """ 526 | for key in ['GEM_PATH', 'LD_LIBRARY_PATH', 'LD_PRELOAD']: 527 | os.environ.pop(key, None) 528 | 529 | 530 | def generate_katello_facts(): 531 | """ 532 | Write katello_facts file based on FQDN. Done after installation 533 | of katello-ca-consumer RPM in case the script is overriding the 534 | FQDN. Place the location if the location option is included 535 | """ 536 | 537 | print_generic("Writing FQDN katello-fact") 538 | katellofacts = open('/etc/rhsm/facts/katello.facts', 'w') 539 | katellofacts.write('{"network.hostname-override":"%s"}\n' % (FQDN)) 540 | katellofacts.close() 541 | 542 | if options.location and 'foreman' in options.skip: 543 | print_generic("Writing LOCATION RHSM fact") 544 | locationfacts = open('/etc/rhsm/facts/location.facts', 'w') 545 | locationfacts.write('{"foreman_location":"%s"}\n' % (options.location)) 546 | locationfacts.close() 547 | 548 | 549 | def install_puppet_agent(): 550 | """Install and configure, then enable and start the Puppet Agent""" 551 | puppet_env = return_puppetenv_for_hg(return_matching_foreman_key('hostgroups', 'title="%s"' % options.hostgroup, 'id', False)) 552 | 553 | # If there is no Puppet environment, skip configuring Puppet 554 | if puppet_env is None: 555 | print_generic("No Puppet environment found, skipping Puppet setup.") 556 | return 557 | 558 | print_generic("Installing the Puppet Agent") 559 | call_yum("install", "puppet-agent") 560 | enable_service("puppet") 561 | 562 | puppet_conf_file = '/etc/puppetlabs/puppet/puppet.conf' 563 | main_section = """[main] 564 | vardir = /opt/puppetlabs/puppet/cache 565 | logdir = /var/log/puppetlabs/puppet 566 | rundir = /var/run/puppetlabs 567 | ssldir = /etc/puppetlabs/puppet/ssl 568 | """ 569 | if is_fips(): 570 | main_section += "digest_algorithm = sha256" 571 | print_generic("System is in FIPS mode. Setting digest_algorithm to SHA256 in puppet.conf") 572 | puppet_conf = open(puppet_conf_file, 'w') 573 | 574 | # set puppet.conf certname to lowercase FQDN, as capitalized characters would 575 | # get translated anyway generating our certificate 576 | # * https://puppet.com/docs/puppet/3.8/configuration.html#certname 577 | # * https://puppet.com/docs/puppet/4.10/configuration.html#certname 578 | # * https://puppet.com/docs/puppet/5.5/configuration.html#certname 579 | # other links mentioning capitalized characters related issues: 580 | # * https://grokbase.com/t/gg/puppet-users/152s27374y/forcing-a-variable-to-be-lower-case 581 | # * https://groups.google.com/forum/#!topic/puppet-users/vRAu092ppzs 582 | puppet_conf.write(""" 583 | %s 584 | [agent] 585 | pluginsync = true 586 | report = true 587 | ignoreschedules = true 588 | daemon = false 589 | ca_server = %s 590 | certname = %s 591 | environment = %s 592 | server = %s 593 | """ % (main_section, options.puppet_ca_server, FQDN.lower(), puppet_env, options.puppet_server)) 594 | if options.puppet_ca_port: 595 | puppet_conf.write("""ca_port = %s 596 | """ % (options.puppet_ca_port)) 597 | if options.puppet_noop: 598 | puppet_conf.write("""noop = true 599 | """) 600 | puppet_conf.close() 601 | noop_puppet_signing_run() 602 | if 'puppet-enable' not in options.skip: 603 | enable_service("puppet") 604 | exec_service("puppet", "restart") 605 | 606 | 607 | def noop_puppet_signing_run(): 608 | """ 609 | Execute Puppet with --noop to generate and sign certs 610 | """ 611 | print_generic("Running Puppet in noop mode to generate SSL certs") 612 | print_generic("Visit the UI and approve this certificate via Infrastructure->Capsules") 613 | print_generic("if auto-signing is disabled") 614 | exec_failexit("/opt/puppetlabs/puppet/bin/puppet agent --test --noop --tags no_such_tag --waitforcert 10") 615 | if 'puppet-enable' not in options.skip: 616 | enable_service("puppet") 617 | exec_service("puppet", "restart") 618 | 619 | 620 | def remove_obsolete_packages(): 621 | """Remove old RHN packages""" 622 | print_generic("Removing old RHN packages") 623 | call_yum("remove", "rhn-setup rhn-client-tools yum-rhn-plugin rhnsd rhn-check rhnlib spacewalk-abrt spacewalk-oscap osad 'rh-*-rhui-client' 'candlepin-cert-consumer-*'", False) 624 | 625 | 626 | def fully_update_the_box(): 627 | """Call `yum -y update` to upgrade the host.""" 628 | print_generic("Fully Updating The Box") 629 | call_yum("update") 630 | 631 | 632 | # curl https://satellite.example.com:9090/ssh/pubkey >> ~/.ssh/authorized_keys 633 | # sort -u ~/.ssh/authorized_keys 634 | def install_ssh_key_from_url(remote_url): 635 | """ 636 | Download and install Foreman's SSH public key. 637 | """ 638 | print_generic("Fetching Remote Execution SSH key from %s" % remote_url) 639 | try: 640 | if sys.version_info >= (2, 6): 641 | foreman_ssh_key_req = urllib_urlopen(remote_url, timeout=options.timeout) 642 | else: 643 | foreman_ssh_key_req = urllib_urlopen(remote_url) 644 | foreman_ssh_key = foreman_ssh_key_req.read() 645 | if sys.version_info >= (3, 0): 646 | foreman_ssh_key = foreman_ssh_key.decode(foreman_ssh_key_req.headers.get_content_charset('utf-8')) 647 | except urllib_httperror: 648 | exception = sys.exc_info()[1] 649 | print_generic("The server was unable to fulfill the request. Error: %s - %s" % (exception.code, exception.reason)) 650 | print_generic("Please ensure the Remote Execution feature is configured properly") 651 | print_warning("Installing Foreman SSH key") 652 | return 653 | except urllib_urlerror: 654 | exception = sys.exc_info()[1] 655 | print_generic("Could not reach the server. Error: %s" % exception.reason) 656 | return 657 | install_ssh_key_from_string(foreman_ssh_key) 658 | 659 | 660 | def install_ssh_key_from_api(): 661 | """ 662 | Download and install all Foreman's SSH public keys. 663 | """ 664 | print_generic("Fetching Remote Execution SSH keys from the Foreman API") 665 | url = "https://" + options.foreman_fqdn + ":" + str(API_PORT) + "/api/v2/smart_proxies/" 666 | smart_proxies = get_json(url) 667 | for smart_proxy in smart_proxies['results']: 668 | if 'remote_execution_pubkey' in smart_proxy: 669 | install_ssh_key_from_string(smart_proxy['remote_execution_pubkey']) 670 | 671 | 672 | def install_ssh_key_from_string(foreman_ssh_key): 673 | """ 674 | Install Foreman's SSH public key into the foreman user's 675 | authorized keys file location, so that remote execution becomes possible. 676 | If not set default is ~/.ssh/authorized_keys 677 | """ 678 | print_generic("Installing Remote Execution SSH key for user %s" % options.remote_exec_user) 679 | foreman_ssh_key = foreman_ssh_key.strip() 680 | userpw = pwd.getpwnam(options.remote_exec_user) 681 | if not options.remote_exec_authpath: 682 | options.remote_exec_authpath = os.path.join(userpw.pw_dir, '.ssh', 'authorized_keys') 683 | foreman_ssh_dir = os.path.join(userpw.pw_dir, '.ssh') 684 | if not os.path.isdir(foreman_ssh_dir): 685 | os.mkdir(foreman_ssh_dir, OWNER_ONLY_DIR) 686 | os.chown(foreman_ssh_dir, userpw.pw_uid, userpw.pw_gid) 687 | elif os.path.exists(options.remote_exec_authpath) and not os.path.isfile(options.remote_exec_authpath): 688 | print_error("Foreman's SSH key not installed. You need to provide a full path to an authorized_keys file, you provided: '%s'" % options.remote_exec_authpath) 689 | return 690 | if os.path.isfile(options.remote_exec_authpath): 691 | if foreman_ssh_key in open(options.remote_exec_authpath, 'r').read(): 692 | print_generic("Foreman's SSH key already present in %s" % options.remote_exec_authpath) 693 | return 694 | output = os.fdopen(os.open(options.remote_exec_authpath, os.O_WRONLY | os.O_CREAT, OWNER_ONLY_FILE), 'a') 695 | output.write("\n") 696 | output.write(foreman_ssh_key) 697 | output.write("\n") 698 | os.chown(options.remote_exec_authpath, userpw.pw_uid, userpw.pw_gid) 699 | print_generic("Foreman's SSH key added to %s" % options.remote_exec_authpath) 700 | output.close() 701 | 702 | 703 | class BetterHTTPErrorProcessor(urllib_basehandler): 704 | """ 705 | A substitute/supplement class to HTTPErrorProcessor 706 | that doesn't raise exceptions on status codes 201,204,206 707 | """ 708 | # pylint:disable=unused-argument,no-self-use,no-init 709 | 710 | def http_error_201(self, request, response, code, msg, hdrs): 711 | """Handle HTTP 201""" 712 | return response 713 | 714 | def http_error_204(self, request, response, code, msg, hdrs): 715 | """Handle HTTP 204""" 716 | return response 717 | 718 | def http_error_206(self, request, response, code, msg, hdrs): 719 | """Handle HTTP 206""" 720 | return response 721 | 722 | 723 | def call_api(url, data=None, method='GET'): 724 | """ 725 | Helper function to place an API call returning JSON results and doing 726 | some error handling. Any error results in an exit. 727 | """ 728 | try: 729 | request = urllib_request(url) 730 | if options.verbose: 731 | print('url: %s' % url) 732 | print('method: %s' % method) 733 | print('data: %s' % json.dumps(data, sort_keys=False, indent=2)) 734 | auth_string = '%s:%s' % (options.login, options.password) 735 | base64string = base64.b64encode(auth_string.encode('utf-8')).decode().strip() 736 | request.add_header("Authorization", "Basic %s" % base64string) 737 | request.add_header("Content-Type", "application/json") 738 | request.add_header("Accept", "application/json") 739 | if data: 740 | if hasattr(request, 'add_data'): 741 | request.add_data(json.dumps(data)) 742 | else: 743 | request.data = json.dumps(data).encode() 744 | request.get_method = lambda: method 745 | if sys.version_info >= (2, 6): 746 | result = urllib_urlopen(request, timeout=options.timeout) 747 | else: 748 | result = urllib_urlopen(request) 749 | jsonresult = json.load(result) 750 | if options.verbose: 751 | print('result: %s' % json.dumps(jsonresult, sort_keys=False, indent=2)) 752 | return jsonresult 753 | except urllib_urlerror: 754 | exception = sys.exc_info()[1] 755 | print('An error occurred: %s' % exception) 756 | print('url: %s' % url) 757 | if isinstance(exception, urllib_httperror): 758 | print('code: %s' % exception.code) # pylint:disable=no-member 759 | if data: 760 | print('data: %s' % json.dumps(data, sort_keys=False, indent=2)) 761 | try: 762 | jsonerr = json.load(exception) 763 | print('error: %s' % json.dumps(jsonerr, sort_keys=False, indent=2)) 764 | except: # noqa: E722, pylint:disable=bare-except 765 | print('error: %s' % exception) 766 | sys.exit(1) 767 | except Exception: # pylint:disable=broad-except 768 | exception = sys.exc_info()[1] 769 | print("FATAL Error - %s" % (exception)) 770 | sys.exit(2) 771 | 772 | 773 | def get_json(url): 774 | """Use `call_api` to place a "GET" REST API call.""" 775 | return call_api(url) 776 | 777 | 778 | def post_json(url, jdata): 779 | """Use `call_api` to place a "POST" REST API call.""" 780 | return call_api(url, data=jdata, method='POST') 781 | 782 | 783 | def delete_json(url): 784 | """Use `call_api` to place a "DELETE" REST API call.""" 785 | return call_api(url, method='DELETE') 786 | 787 | 788 | def put_json(url, jdata=None): 789 | """Use `call_api` to place a "PUT" REST API call.""" 790 | return call_api(url, data=jdata, method='PUT') 791 | 792 | 793 | def update_host_capsule_mapping(attribute, capsule_id, host_id): 794 | """ 795 | Update the host entry to point a feature to a new proxy 796 | """ 797 | url = "https://" + options.foreman_fqdn + ":" + str(API_PORT) + "/api/v2/hosts/" + str(host_id) 798 | if attribute == 'content_source_id': 799 | jdata = {"host": {"content_facet_attributes": {"content_source_id": capsule_id}, "content_source_id": capsule_id}} 800 | else: 801 | jdata = {"host": {attribute: capsule_id}} 802 | return put_json(url, jdata) 803 | 804 | 805 | def get_capsule_features(capsule_id): 806 | """ 807 | Fetch all features available on a proxy 808 | """ 809 | url = "https://" + options.foreman_fqdn + ":" + str(API_PORT) + "/api/smart_proxies/%s" % str(capsule_id) 810 | return [feature['name'] for feature in get_json(url)['features']] 811 | 812 | 813 | def update_host_config(attribute, value, host_id): 814 | """ 815 | Update a host config 816 | """ 817 | attribute_id = return_matching_foreman_key(attribute + 's', 'title="%s"' % value, 'id', False) 818 | json_key = attribute + "_id" 819 | jdata = {"host": {json_key: attribute_id}} 820 | put_json("https://" + options.foreman_fqdn + ":" + API_PORT + "/api/hosts/%s" % host_id, jdata) 821 | 822 | 823 | def return_matching_foreman_key(api_name, search_key, return_key, null_result_ok=False): 824 | """ 825 | Function uses `return_matching_key` to make an API call to Foreman. 826 | """ 827 | return return_matching_key("/api/v2/" + api_name, search_key, return_key, null_result_ok) 828 | 829 | 830 | def return_matching_katello_key(api_name, search_key, return_key, null_result_ok=False): 831 | """ 832 | Function uses `return_matching_key` to make an API call to Katello. 833 | """ 834 | return return_matching_key("/katello/api/" + api_name, search_key, return_key, null_result_ok) 835 | 836 | 837 | def return_matching_key(api_path, search_key, return_key, null_result_ok=False): 838 | """ 839 | Search in API given a search key, which must be unique, then returns the 840 | field given in "return_key" as ID. 841 | api_path is the path in url for API name, search_key must contain also 842 | the key for search (name=, title=, ...). 843 | The search_key must be quoted in advance. 844 | """ 845 | myurl = "https://" + options.foreman_fqdn + ":" + API_PORT + api_path + "/?" + urlencode([('search', '' + str(search_key))]) 846 | return_values = get_json(myurl) 847 | result_len = len(return_values['results']) 848 | if result_len == 1: 849 | return_values_return_key = return_values['results'][0][return_key] 850 | elif result_len == 0 and null_result_ok is True: 851 | return_values_return_key = None 852 | else: 853 | print_error("%d element in array for search key '%s' in API '%s'. Please note that all searches are case-sensitive." % (result_len, search_key, api_path)) 854 | print_error("Please also ensure that the user has permissions to view the searched objects. Fatal error.") 855 | sys.exit(2) 856 | 857 | return return_values_return_key 858 | 859 | 860 | def return_puppetenv_for_hg(hg_id): 861 | """ 862 | Return the Puppet environment of the given hostgroup ID, either directly 863 | or inherited through its hierarchy. If no environment is found, 864 | `None` is returned. 865 | """ 866 | myurl = "https://" + options.foreman_fqdn + ":" + API_PORT + "/api/v2/hostgroups/" + str(hg_id) 867 | hostgroup = get_json(myurl) 868 | environment_name = hostgroup.get('environment_name') 869 | if not environment_name and hostgroup.get('ancestry'): 870 | parent = hostgroup['ancestry'].split('/')[-1] 871 | environment_name = return_puppetenv_for_hg(parent) 872 | return environment_name 873 | 874 | 875 | def create_domain(domain, orgid, locid): 876 | """ 877 | Call Foreman API to create a network domain associated with the given 878 | organization and location. 879 | """ 880 | myurl = "https://" + options.foreman_fqdn + ":" + API_PORT + "/api/v2/domains" 881 | domid = return_matching_foreman_key('domains', 'name="%s"' % domain, 'id', True) 882 | if not domid: 883 | jsondata = {"domain": {"name": domain, "organization_ids": [orgid], "location_ids": [locid]}} 884 | print_running("Calling Foreman API to create domain %s associated with the org & location" % domain) 885 | post_json(myurl, jsondata) 886 | 887 | 888 | def create_host(): 889 | 890 | # pylint:disable=too-many-branches 891 | # pylint:disable=too-many-statements 892 | 893 | """ 894 | Call Foreman API to create a host entry associated with the 895 | host group, organization & location, domain and architecture. 896 | """ 897 | myhgid = return_matching_foreman_key('hostgroups', 'title="%s"' % options.hostgroup, 'id', False) 898 | if options.location: 899 | mylocid = return_matching_foreman_key('locations', 'title="%s"' % options.location, 'id', False) 900 | else: 901 | mylocid = None 902 | myorgid = return_matching_foreman_key('organizations', 'name="%s"' % options.org, 'id', False) 903 | if DOMAIN: 904 | if options.add_domain: 905 | create_domain(DOMAIN, myorgid, mylocid) 906 | 907 | mydomainid = return_matching_foreman_key('domains', 'name="%s"' % DOMAIN, 'id', True) 908 | if not mydomainid: 909 | print_generic("Domain %s doesn't exist in Foreman, consider using the --add-domain option." % DOMAIN) 910 | sys.exit(2) 911 | domain_available_search = 'name="%s"&organization_id=%s' % (DOMAIN, myorgid) 912 | if mylocid: 913 | domain_available_search += '&location_id=%s' % (mylocid) 914 | mydomainid = return_matching_foreman_key('domains', domain_available_search, 'id', True) 915 | if not mydomainid: 916 | print_generic("Domain %s exists in Foreman, but is not assigned to the requested Organization or Location." % DOMAIN) 917 | sys.exit(2) 918 | else: 919 | mydomainid = None 920 | if options.force_content_source: 921 | my_content_src_id = return_matching_foreman_key(api_name='smart_proxies', search_key='name="%s"' % options.foreman_fqdn, return_key='id', null_result_ok=True) 922 | if my_content_src_id is None: 923 | print_warning("You requested to set the content source to %s, but we could not find such a Smart Proxy configured. The content source WILL NOT be updated!" % (options.foreman_fqdn,)) 924 | else: 925 | my_content_src_id = None 926 | architecture_id = return_matching_foreman_key('architectures', 'name="%s"' % ARCHITECTURE, 'id', False) 927 | host_id = return_matching_foreman_key('hosts', 'name="%s"' % FQDN, 'id', True) 928 | # create the starting json, to be filled below 929 | jsondata = {"host": {"name": HOSTNAME, "hostgroup_id": myhgid, "organization_id": myorgid, "mac": MAC, "architecture_id": architecture_id, "build": False, "compute_resource_id": None}} 930 | # optional parameters 931 | if my_content_src_id: 932 | jsondata['host']['content_facet_attributes'] = {'content_source_id': my_content_src_id} 933 | if options.operatingsystem is not None: 934 | operatingsystem_id = return_matching_foreman_key('operatingsystems', 'title="%s"' % options.operatingsystem, 'id', False) 935 | jsondata['host']['operatingsystem_id'] = operatingsystem_id 936 | if options.partitiontable is not None: 937 | partitiontable_id = return_matching_foreman_key('ptables', 'name="%s"' % options.partitiontable, 'id', False) 938 | jsondata['host']['ptable_id'] = partitiontable_id 939 | if not options.unmanaged: 940 | jsondata['host']['managed'] = 'true' 941 | else: 942 | jsondata['host']['managed'] = 'false' 943 | if mylocid: 944 | jsondata['host']['location_id'] = mylocid 945 | if mydomainid: 946 | jsondata['host']['domain_id'] = mydomainid 947 | if options.ip: 948 | jsondata['host']['ip'] = options.ip 949 | if options.comment: 950 | jsondata['host']['comment'] = options.comment 951 | myurl = "https://" + options.foreman_fqdn + ":" + API_PORT + "/api/v2/hosts/" 952 | if options.force and host_id is not None: 953 | disassociate_host(host_id) 954 | delete_host(host_id) 955 | print_running("Calling Foreman API to create a host entry associated with the group & org") 956 | post_json(myurl, jsondata) 957 | print_success("Successfully created host %s" % FQDN) 958 | 959 | 960 | def delete_host(host_id): 961 | """Call Foreman API to delete the current host.""" 962 | myurl = "https://" + options.foreman_fqdn + ":" + API_PORT + "/api/v2/hosts/" 963 | print_running("Deleting host id %s for host %s" % (host_id, FQDN)) 964 | delete_json("%s/%s" % (myurl, host_id)) 965 | 966 | 967 | def disassociate_host(host_id): 968 | """ 969 | Call Foreman API to disassociate host from content host before deletion. 970 | """ 971 | myurl = "https://" + options.foreman_fqdn + ":" + API_PORT + "/api/v2/hosts/" + str(host_id) + "/disassociate" 972 | print_running("Disassociating host id %s for host %s" % (host_id, FQDN)) 973 | put_json(myurl) 974 | 975 | 976 | def configure_subscription_manager(): 977 | """ 978 | Configure subscription-manager plugins in Yum 979 | """ 980 | productidconfig = SafeConfigParser() 981 | productidconfig.read('/etc/yum/pluginconf.d/product-id.conf') 982 | if productidconfig.get('main', 'enabled') == '0': 983 | print_generic("Product-id yum plugin was disabled. Enabling...") 984 | productidconfig.set('main', 'enabled', '1') 985 | productidconfig.write(open('/etc/yum/pluginconf.d/product-id.conf', 'w')) 986 | 987 | submanconfig = SafeConfigParser() 988 | submanconfig.read('/etc/yum/pluginconf.d/subscription-manager.conf') 989 | if submanconfig.get('main', 'enabled') == '0': 990 | print_generic("subscription-manager yum plugin was disabled. Enabling...") 991 | submanconfig.set('main', 'enabled', '1') 992 | submanconfig.write(open('/etc/yum/pluginconf.d/subscription-manager.conf', 'w')) 993 | 994 | 995 | def check_rhn_registration(): 996 | """Helper function to check if host is registered to legacy RHN.""" 997 | if os.path.exists('/etc/sysconfig/rhn/systemid'): 998 | retcode = getstatusoutput('rhn-channel -l')[0] 999 | if NEED_STATUS_SHIFT: 1000 | retcode = os.WEXITSTATUS(retcode) 1001 | return retcode == 0 1002 | return False 1003 | 1004 | 1005 | def enable_repos(): 1006 | """Enable necessary repositories using subscription-manager.""" 1007 | repostoenable = " ".join(['--enable=%s' % i for i in options.enablerepos.split(',')]) 1008 | print_running("Enabling repositories - %s" % options.enablerepos) 1009 | exec_failok("subscription-manager repos %s" % repostoenable) 1010 | 1011 | 1012 | def install_packages(): 1013 | """Install user-provided packages""" 1014 | packages = options.install_packages.replace(',', " ") 1015 | print_running("Installing the following packages %s" % packages) 1016 | call_yum("install", packages, False) 1017 | 1018 | 1019 | def get_api_port(foreman_fqdn): 1020 | """ 1021 | Helper function to get the API port. 1022 | 1023 | This tries to access the `/api/ping` endoint in Foreman which exists since 1.22 and does 1024 | not require any authentication. 1025 | The detection can fail if the TLS certificate of the remote isn't trusted, so this has 1026 | to be called *after* katello-ca-consumer RPM has been installed and added the CA to the 1027 | system trust store. 1028 | The detection can also fail if there is no API reverse proxy available on the 1029 | Foreman Proxy. 1030 | If the detection fails we still return a fallback of "443" to allow the "normal" error 1031 | handling to catch the issue later on - or ignore it if API access it not required. 1032 | """ 1033 | for port in ['443', '8443']: 1034 | try: 1035 | request = urllib_request('https://' + foreman_fqdn + ':' + port + '/api/ping') 1036 | request.add_header("Content-Type", "application/json") 1037 | request.add_header("Accept", "application/json") 1038 | urllib_urlopen(request, timeout=options.timeout) 1039 | return port 1040 | except: # noqa: E722, pylint:disable=bare-except 1041 | pass 1042 | return "443" 1043 | 1044 | 1045 | def get_rhsm_port(): 1046 | """Helper function to get the RHSM port from Subscription Manager.""" 1047 | configparser = SafeConfigParser() 1048 | configparser.read('/etc/rhsm/rhsm.conf') 1049 | try: 1050 | return configparser.get('server', 'port') 1051 | except: # noqa: E722, pylint:disable=bare-except 1052 | return "443" 1053 | 1054 | 1055 | def check_rpm_installed(): 1056 | """Check if the machine already has Katello/Spacewalk RPMs installed""" 1057 | rpm_sat = ['katello', 'foreman-proxy-content', 'katello-capsule', 'spacewalk-proxy-common', 'spacewalk-backend'] 1058 | transaction_set = rpm.TransactionSet() 1059 | db_results = transaction_set.dbMatch() 1060 | for package in db_results: 1061 | try: 1062 | package_name = package['name'].decode('ascii') 1063 | except AttributeError: 1064 | package_name = package['name'] 1065 | if package_name in rpm_sat: 1066 | print_error("%s RPM found. bootstrap.py should not be used on a Katello/Spacewalk/Satellite host." % (package_name)) 1067 | sys.exit(1) 1068 | 1069 | 1070 | def prepare_rhel5_migration(): 1071 | """ 1072 | Execute specific preparations steps for RHEL 5. Older releases of RHEL 5 1073 | did not have a version of rhn-classic-migrate-to-rhsm which supported 1074 | activation keys. This function allows those systems to get a proper 1075 | product certificate. 1076 | """ 1077 | install_prereqs() 1078 | 1079 | # only do the certificate magic if 69.pem is not present 1080 | # and we have active channels 1081 | if check_rhn_registration() and not os.path.exists('/etc/pki/product/69.pem'): 1082 | # pylint:disable=W,C,R 1083 | _LIBPATH = "/usr/share/rhsm" 1084 | # add to the path if need be 1085 | if _LIBPATH not in sys.path: 1086 | sys.path.append(_LIBPATH) 1087 | from subscription_manager.migrate import migrate # pylint:disable=import-error 1088 | 1089 | class MEOptions: 1090 | force = True 1091 | 1092 | me = migrate.MigrationEngine() 1093 | me.options = MEOptions() 1094 | subscribed_channels = me.get_subscribed_channels_list() 1095 | me.print_banner(("System is currently subscribed to these RHNClassic Channels:")) 1096 | for channel in subscribed_channels: 1097 | print(channel) 1098 | me.check_for_conflicting_channels(subscribed_channels) 1099 | me.deploy_prod_certificates(subscribed_channels) 1100 | me.clean_up(subscribed_channels) 1101 | 1102 | # at this point we should have at least 69.pem available, but lets 1103 | # doublecheck and copy it manually if not 1104 | if not os.path.exists('/etc/pki/product/'): 1105 | os.mkdir("/etc/pki/product/") 1106 | mapping_file = "/usr/share/rhsm/product/RHEL-5/channel-cert-mapping.txt" 1107 | if not os.path.exists('/etc/pki/product/69.pem') and os.path.exists(mapping_file): 1108 | for line in open(mapping_file): 1109 | if line.startswith('rhel-%s-server-5' % ARCHITECTURE): 1110 | cert = line.split(" ")[1] 1111 | shutil.copy('/usr/share/rhsm/product/RHEL-5/' + cert.strip(), 1112 | '/etc/pki/product/69.pem') 1113 | break 1114 | 1115 | # cleanup 1116 | disable_rhn_plugin() 1117 | 1118 | 1119 | def enable_service(service, failonerror=True): 1120 | """ 1121 | Helper function to enable a service using proper init system. 1122 | pass failonerror = False to make init system's commands non-fatal 1123 | """ 1124 | if os.path.exists("/run/systemd"): 1125 | exec_command("/usr/bin/systemctl enable %s" % (service), not failonerror) 1126 | else: 1127 | exec_command("/sbin/chkconfig %s on" % (service), not failonerror) 1128 | 1129 | 1130 | def exec_service(service, command, failonerror=True): 1131 | """ 1132 | Helper function to call a service command using proper init system. 1133 | Available command values = start, stop, restart 1134 | pass failonerror = False to make init system's commands non-fatal 1135 | """ 1136 | if os.path.exists("/run/systemd"): 1137 | exec_command("/usr/bin/systemctl %s %s" % (command, service), not failonerror) 1138 | else: 1139 | exec_command("/sbin/service %s %s" % (service, command), not failonerror) 1140 | 1141 | 1142 | def release_from_etc(): 1143 | """ 1144 | Parse /etc/os-release and get the release version (7, 7.9, 8, etc) from it. 1145 | Used on systems with Python 3.8+ which doesn't provide that in the platform module anymore. 1146 | """ 1147 | release = "8" 1148 | os_release = open('/etc/os-release') 1149 | for line in os_release.readlines(): 1150 | if line.startswith('VERSION_ID='): 1151 | release = line.replace('VERSION_ID=', '').replace('"', '').strip() 1152 | return release 1153 | 1154 | 1155 | if __name__ == '__main__': 1156 | 1157 | # pylint:disable=invalid-name 1158 | 1159 | print("Foreman Bootstrap Script") 1160 | print("This script is designed to register new systems or to migrate an existing system to a Foreman server with Katello") 1161 | 1162 | # > Register our better HTTP processor as default opener for URLs. 1163 | opener = urllib_build_opener(BetterHTTPErrorProcessor) 1164 | urllib_install_opener(opener) 1165 | 1166 | # > Gather MAC Address. 1167 | MAC = None 1168 | try: 1169 | import uuid 1170 | mac1 = uuid.getnode() 1171 | mac2 = uuid.getnode() 1172 | if mac1 == mac2: 1173 | MAC = ':'.join(("%012X" % mac1)[i:i + 2] for i in range(0, 12, 2)) 1174 | except ImportError: 1175 | if os.path.exists('/sys/class/net/eth0/address'): 1176 | address_files = ['/sys/class/net/eth0/address'] 1177 | else: 1178 | address_files = glob.glob('/sys/class/net/*/address') 1179 | for f in address_files: 1180 | MAC = open(f).readline().strip().upper() 1181 | if MAC != "00:00:00:00:00:00": 1182 | break 1183 | if not MAC: 1184 | MAC = "00:00:00:00:00:00" 1185 | 1186 | # > Gather API port (HTTPS), ARCHITECTURE and (OS) RELEASE 1187 | API_PORT = "443" 1188 | RHSM_PORT = "443" 1189 | ARCHITECTURE = get_architecture() 1190 | try: 1191 | # pylint:disable=deprecated-method 1192 | RELEASE = platform.linux_distribution()[1] 1193 | except AttributeError: 1194 | try: 1195 | # pylint:disable=deprecated-method,no-member 1196 | RELEASE = platform.dist()[1] 1197 | except AttributeError: 1198 | RELEASE = release_from_etc() 1199 | IS_EL5 = int(RELEASE[0]) == 5 1200 | IS_EL8 = int(RELEASE[0]) == 8 1201 | if not IS_EL5: 1202 | DEFAULT_DOWNLOAD_METHOD = 'https' 1203 | else: 1204 | DEFAULT_DOWNLOAD_METHOD = 'http' 1205 | 1206 | SKIP_STEPS = ['foreman', 'puppet', 'migration', 'prereq-update', 'katello-agent', 'remove-obsolete-packages', 'puppet-enable', 'katello-host-tools'] 1207 | 1208 | # > Define and parse the options 1209 | usage_string = "Usage: %prog -l admin -s foreman.example.com -o 'Default Organization' -L 'Default Location' -g My_Hostgroup -a My_Activation_Key" 1210 | parser = OptionParser(usage=usage_string, version="%%prog %s" % VERSION) 1211 | parser.add_option("-s", "--server", dest="foreman_fqdn", help="FQDN of Foreman OR Capsule - omit https://", metavar="foreman_fqdn") 1212 | parser.add_option("-l", "--login", dest="login", default='admin', help="Login user for API Calls", metavar="LOGIN") 1213 | parser.add_option("-p", "--password", dest="password", help="Password for specified user. Will prompt if omitted", metavar="PASSWORD") 1214 | parser.add_option("--fqdn", dest="fqdn", help="Set an explicit FQDN, overriding detected FQDN from socket.getfqdn(), currently detected as %default", metavar="FQDN", default=socket.getfqdn()) 1215 | parser.add_option("--legacy-login", dest="legacy_login", default='admin', help="Login user for Satellite 5 API Calls", metavar="LOGIN") 1216 | parser.add_option("--legacy-password", dest="legacy_password", help="Password for specified Satellite 5 user. Will prompt if omitted", metavar="PASSWORD") 1217 | parser.add_option("--legacy-purge", dest="legacy_purge", action="store_true", help="Purge system from the Legacy environment (e.g. Sat5)") 1218 | parser.add_option("-a", "--activationkey", dest="activationkey", help="Activation Key to register the system", metavar="ACTIVATIONKEY") 1219 | parser.add_option("-P", "--skip-puppet", dest="no_puppet", action="store_true", default=False, help="Do not install Puppet") 1220 | parser.add_option("--skip-foreman", dest="no_foreman", action="store_true", default=False, help="Do not create a Foreman host. Implies --skip-puppet. When using --skip-foreman, you MUST pass the Organization's LABEL, not NAME") 1221 | parser.add_option("--force-content-source", dest="force_content_source", action="store_true", default=False, help="Force the content source to be the registration capsule (it overrides the value in the host group if any is defined)") 1222 | parser.add_option("--content-only", dest="content_only", action="store_true", default=False, 1223 | help="Setup host for content only. Alias to --skip foreman. Implies --skip-puppet. When using --content-only, you MUST pass the Organization's LABEL, not NAME") 1224 | parser.add_option("-g", "--hostgroup", dest="hostgroup", help="Title of the Hostgroup in Foreman that the host is to be associated with", metavar="HOSTGROUP") 1225 | parser.add_option("-L", "--location", dest="location", help="Title of the Location in Foreman that the host is to be associated with", metavar="LOCATION") 1226 | parser.add_option("-O", "--operatingsystem", dest="operatingsystem", default=None, help="Title of the Operating System in Foreman that the host is to be associated with", metavar="OPERATINGSYSTEM") 1227 | parser.add_option("--partitiontable", dest="partitiontable", default=None, help="Name of the Partition Table in Foreman that the host is to be associated with", metavar="PARTITIONTABLE") 1228 | parser.add_option("-o", "--organization", dest="org", default='Default Organization', help="Name of the Organization in Foreman that the host is to be associated with", metavar="ORG") 1229 | parser.add_option("-S", "--subscription-manager-args", dest="smargs", default="", help="Which additional arguments shall be passed to subscription-manager", metavar="ARGS") 1230 | parser.add_option("--rhn-migrate-args", dest="rhsmargs", default="", help="Which additional arguments shall be passed to rhn-migrate-classic-to-rhsm", metavar="ARGS") 1231 | parser.add_option("-u", "--update", dest="update", action="store_true", help="Fully Updates the System") 1232 | parser.add_option("-v", "--verbose", dest="verbose", action="store_true", help="Verbose output") 1233 | parser.add_option("-f", "--force", dest="force", action="store_true", help="Force registration (will erase old katello and puppet certs)") 1234 | parser.add_option("--add-domain", dest="add_domain", action="store_true", help="Automatically add the clients domain to Foreman") 1235 | parser.add_option("--puppet-noop", dest="puppet_noop", action="store_true", help="Configure Puppet agent to only run in noop mode") 1236 | parser.add_option("--puppet-server", dest="puppet_server", action="store", help="Configure Puppet agent to use this server as master (defaults to the Foreman server)") 1237 | parser.add_option("--puppet-ca-server", dest="puppet_ca_server", action="store", help="Configure Puppet agent to use this server as CA (defaults to the Foreman server)") 1238 | parser.add_option("--puppet-ca-port", dest="puppet_ca_port", action="store", help="Configure Puppet agent to use this port to connect to the CA") 1239 | parser.add_option("--remove", dest="remove", action="store_true", help="Instead of registering the machine to Foreman remove it") 1240 | parser.add_option("-r", "--release", dest="release", help="Specify release version") 1241 | parser.add_option("-R", "--remove-obsolete-packages", dest="removepkgs", action="store_true", help="Remove old Red Hat Network and RHUI Packages (default)", default=True) 1242 | parser.add_option("--download-method", dest="download_method", default=DEFAULT_DOWNLOAD_METHOD, help="Method to download katello-ca-consumer package (e.g. http or https)", metavar="DOWNLOADMETHOD", choices=['http', 'https']) 1243 | parser.add_option("--no-remove-obsolete-packages", dest="removepkgs", action="store_false", help="Don't remove old Red Hat Network and RHUI Packages") 1244 | parser.add_option("--unmanaged", dest="unmanaged", action="store_true", help="Add the server as unmanaged. Useful to skip provisioning dependencies.") 1245 | parser.add_option("--rex", dest="remote_exec", action="store_true", help="Install Foreman's SSH key for remote execution.", default=False) 1246 | parser.add_option("--rex-user", dest="remote_exec_user", default="root", help="Local user used by Foreman's remote execution feature.") 1247 | parser.add_option("--rex-proxies", dest="remote_exec_proxies", help="Comma separated list of proxies to install Foreman's SSH keys for remote execution.") 1248 | parser.add_option("--rex-urlkeyfile", dest="remote_exec_url", help="HTTP/S location of a file containing one or more Foreman's SSH keys for remote execution.") 1249 | parser.add_option("--rex-apikeys", dest="remote_exec_apikeys", action="store_true", help="Fetch Foreman's SSH keys from the API.") 1250 | parser.add_option("--rex-authpath", dest="remote_exec_authpath", help="Full path to local authorized_keys file in order to install Foreman's SSH keys for remote execution. Default ~/.ssh/authorized_keys") 1251 | parser.add_option("--enablerepos", dest="enablerepos", help="Repositories to be enabled via subscription-manager - comma separated", metavar="enablerepos") 1252 | parser.add_option("--skip", dest="skip", action="append", help="Skip the listed steps (choices: %s)" % SKIP_STEPS, choices=SKIP_STEPS, default=[]) 1253 | parser.add_option("--ip", dest="ip", help="IPv4 address of the primary interface in Foreman (defaults to the address used to make request to Foreman)") 1254 | parser.add_option("--deps-repository-url", dest="deps_repository_url", help="URL to a repository that contains the subscription-manager RPMs") 1255 | parser.add_option("--deps-repository-gpg-key", dest="deps_repository_gpg_key", help="GPG Key to the repository that contains the subscription-manager RPMs", default="file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release") 1256 | parser.add_option("--install-packages", dest="install_packages", help="List of packages to be additionally installed - comma separated", metavar="installpackages") 1257 | parser.add_option("--new-capsule", dest="new_capsule", action="store_true", help="Switch the server to a new capsule for content and Puppet. Pass --server with the Capsule FQDN as well.") 1258 | parser.add_option("-t", "--timeout", dest="timeout", type="int", help="Timeout (in seconds) for API calls and subscription-manager registration. Defaults to %default", metavar="timeout", default=900) 1259 | parser.add_option("-c", "--comment", dest="comment", help="Add a host comment") 1260 | parser.add_option("--ignore-registration-failures", dest="ignore_registration_failures", action="store_true", help="Continue running even if registration via subscription-manager/rhn-migrate-classic-to-rhsm returns a non-zero return code.") 1261 | parser.add_option("--preserve-rhsm-proxy", dest="preserve_rhsm_proxy", action="store_true", help="Preserve proxy settings in /etc/rhsm/rhsm.conf when migrating RHSM -> RHSM") 1262 | parser.add_option("--install-katello-agent", dest="install_katello_agent", action="store_true", help="Installs the Katello Agent", default=False) 1263 | (options, args) = parser.parse_args() 1264 | 1265 | if options.no_foreman: 1266 | print_warning("The --skip-foreman option is deprecated, please use --skip foreman.") 1267 | options.skip.append('foreman') 1268 | if options.no_puppet: 1269 | print_warning("The --skip-puppet option is deprecated, please use --skip puppet.") 1270 | options.skip.append('puppet') 1271 | if not options.removepkgs: 1272 | options.skip.append('remove-obsolete-packages') 1273 | if options.content_only: 1274 | print_generic("The --content-only option was provided. Adding --skip foreman") 1275 | options.skip.append('foreman') 1276 | if not options.puppet_server: 1277 | options.puppet_server = options.foreman_fqdn 1278 | if not options.puppet_ca_server: 1279 | options.puppet_ca_server = options.foreman_fqdn 1280 | 1281 | # > Validate that the options make sense or exit with a message. 1282 | # the logic is as follows: 1283 | # if mode = create: 1284 | # foreman_fqdn 1285 | # org 1286 | # activation_key 1287 | # if foreman: 1288 | # hostgroup 1289 | # else if mode = remove: 1290 | # if removing from foreman: 1291 | # foreman_fqdn 1292 | if not ((options.remove and ('foreman' in options.skip or options.foreman_fqdn)) or 1293 | (options.foreman_fqdn and options.org and options.activationkey and ('foreman' in options.skip or options.hostgroup)) or 1294 | (options.foreman_fqdn and options.new_capsule)): 1295 | if not options.remove and not options.new_capsule: 1296 | print("Must specify server, login, organization, hostgroup and activation key. See usage:") 1297 | elif options.new_capsule: 1298 | print("Must use both --new-capsule and --server. See usage:") 1299 | else: 1300 | print("Must specify server. See usage:") 1301 | parser.print_help() 1302 | sys.exit(1) 1303 | 1304 | # > Gather FQDN, HOSTNAME and DOMAIN using options.fqdn 1305 | # > If socket.fqdn() returns an FQDN, derive HOSTNAME & DOMAIN using FQDN 1306 | # > else, HOSTNAME isn't an FQDN 1307 | # > if user passes --fqdn set FQDN, HOSTNAME and DOMAIN to the parameter that is given. 1308 | FQDN = options.fqdn 1309 | if FQDN.find(".") != -1: 1310 | HOSTNAME = FQDN.split('.')[0] 1311 | DOMAIN = FQDN[FQDN.index('.') + 1:] 1312 | else: 1313 | HOSTNAME = FQDN 1314 | DOMAIN = None 1315 | 1316 | # > Exit if DOMAIN isn't set and Puppet must be installed (without force) 1317 | if not DOMAIN and not (options.force or 'puppet' in options.skip): 1318 | print("We could not determine the domain of this machine, most probably `hostname -f` does not return the FQDN.") 1319 | print("This can lead to Puppet misbehaviour and thus the script will terminate now.") 1320 | print("You can override this by passing one of the following") 1321 | print("\t--force - to disable all checking") 1322 | print("\t--skip puppet - to omit installing the puppet agent") 1323 | print("\t--fqdn - to set an explicit FQDN, overriding detected FQDN") 1324 | sys.exit(1) 1325 | 1326 | # > Gather primary IP address if none was given 1327 | # we do this *after* parsing options to find the IP on the interface 1328 | # towards the Foreman instance in the case the machine has multiple 1329 | if not options.ip: 1330 | try: 1331 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 1332 | s.connect((options.foreman_fqdn, 80)) 1333 | options.ip = s.getsockname()[0] 1334 | s.close() 1335 | except: # noqa: E722, pylint:disable=bare-except 1336 | options.ip = None 1337 | 1338 | # > Ask for the password if not given as option 1339 | if not options.password and 'foreman' not in options.skip: 1340 | options.password = getpass.getpass("%s's password:" % options.login) 1341 | 1342 | # > If user wants to purge profile from RHN/Satellite 5, credentials are needed. 1343 | if options.legacy_purge and not options.legacy_password: 1344 | options.legacy_password = getpass.getpass("Legacy User %s's password:" % options.legacy_login) 1345 | 1346 | # > Puppet won't be installed if Foreman Host shall not be created 1347 | if 'foreman' in options.skip: 1348 | options.skip.append('puppet') 1349 | 1350 | options.skip = set(options.skip) 1351 | 1352 | # > Output all parameters if verbose. 1353 | if options.verbose: 1354 | print("HOSTNAME - %s" % HOSTNAME) 1355 | print("DOMAIN - %s" % DOMAIN) 1356 | print("FQDN - %s" % FQDN) 1357 | print("OS RELEASE - %s" % RELEASE) 1358 | print("MAC - %s" % MAC) 1359 | print("IP - %s" % options.ip) 1360 | print("foreman_fqdn - %s" % options.foreman_fqdn) 1361 | print("LOGIN - %s" % options.login) 1362 | print("PASSWORD - %s" % options.password) 1363 | print("HOSTGROUP - %s" % options.hostgroup) 1364 | print("LOCATION - %s" % options.location) 1365 | print("OPERATINGSYSTEM - %s" % options.operatingsystem) 1366 | print("PARTITIONTABLE - %s" % options.partitiontable) 1367 | print("ORG - %s" % options.org) 1368 | print("ACTIVATIONKEY - %s" % options.activationkey) 1369 | print("CONTENT RELEASE - %s" % options.release) 1370 | print("UPDATE - %s" % options.update) 1371 | print("LEGACY LOGIN - %s" % options.legacy_login) 1372 | print("LEGACY PASSWORD - %s" % options.legacy_password) 1373 | print("DOWNLOAD METHOD - %s" % options.download_method) 1374 | print("SKIP - %s" % options.skip) 1375 | print("TIMEOUT - %s" % options.timeout) 1376 | print("PUPPET SERVER - %s" % options.puppet_server) 1377 | print("PUPPET CA SERVER - %s" % options.puppet_ca_server) 1378 | print("PUPPET CA PORT - %s" % options.puppet_ca_port) 1379 | print("IGNORE REGISTRATION FAILURES - %s" % options.ignore_registration_failures) 1380 | print("PRESERVE RHSM PROXY CONFIGURATION - %s" % options.preserve_rhsm_proxy) 1381 | print("REX - %s" % options.remote_exec) 1382 | if options.remote_exec: 1383 | print("REX USER - %s" % options.remote_exec_user) 1384 | print("REX PROXIES - %s" % options.remote_exec_proxies) 1385 | print("REX KEY URL - %s" % options.remote_exec_url) 1386 | print("REX KEYS FROM API - %s" % options.remote_exec_apikeys) 1387 | print("REX AUTHPATH - %s" % options.remote_exec_authpath) 1388 | 1389 | # > Exit if the user isn't root. 1390 | # Done here to allow an unprivileged user to run the script to see 1391 | # its various options. 1392 | if os.getuid() != 0: 1393 | print_error("This script requires root-level access") 1394 | sys.exit(1) 1395 | 1396 | # > Check if Katello/Spacewalk/Satellite are installed already 1397 | check_rpm_installed() 1398 | 1399 | # > Try to import json or simplejson. 1400 | # do it at this point in the code to have our custom print and exec 1401 | # functions available 1402 | try: 1403 | import json 1404 | except ImportError: 1405 | try: 1406 | import simplejson as json 1407 | except ImportError: 1408 | print_warning("Could neither import json nor simplejson, will try to install simplejson and re-import") 1409 | call_yum("install", "python-simplejson") 1410 | try: 1411 | import simplejson as json 1412 | except ImportError: 1413 | print_error("Could not install python-simplejson") 1414 | sys.exit(1) 1415 | 1416 | # > Clean the environment from LD_... variables 1417 | clean_environment() 1418 | 1419 | # > IF RHEL 5, not removing, and not moving to new capsule prepare the migration. 1420 | if not options.remove and IS_EL5 and not options.new_capsule: 1421 | if options.legacy_purge: 1422 | print_warning("Purging the system from the Legacy environment is not supported on EL5.") 1423 | prepare_rhel5_migration() 1424 | 1425 | if options.preserve_rhsm_proxy: 1426 | saved_proxy_config = get_rhsm_proxy() 1427 | 1428 | if options.remove: 1429 | # > IF remove, disassociate/delete host, unregister, 1430 | # > uninstall katello and optionally puppet agents 1431 | API_PORT = get_api_port(options.foreman_fqdn) 1432 | RHSM_PORT = get_rhsm_port() 1433 | unregister_system() 1434 | if 'foreman' not in options.skip: 1435 | hostid = return_matching_foreman_key('hosts', 'name="%s"' % FQDN, 'id', True) 1436 | if hostid is not None: 1437 | disassociate_host(hostid) 1438 | delete_host(hostid) 1439 | if 'katello-agent' in options.skip: 1440 | print_warning("Skipping the installation of the Katello Agent is now the default behavior. passing --skip katello-agent is deprecated") 1441 | if 'katello-agent' not in options.skip or 'katello-host-tools' not in options.skip: 1442 | clean_katello_agent() 1443 | if 'puppet' not in options.skip: 1444 | clean_puppet() 1445 | elif check_rhn_registration() and 'migration' not in options.skip and not IS_EL8: 1446 | # > ELIF registered to RHN, install subscription-manager prerequs 1447 | # > get CA RPM, optionally create host, 1448 | # > migrate via rhn-classic-migrate-to-rhsm 1449 | print_generic('This system is registered to RHN. Attempting to migrate via rhn-classic-migrate-to-rhsm') 1450 | install_prereqs() 1451 | 1452 | _, versionerr = check_migration_version(SUBSCRIPTION_MANAGER_MIGRATION_MINIMAL_VERSION) 1453 | if versionerr: 1454 | print_error(versionerr) 1455 | sys.exit(1) 1456 | 1457 | get_bootstrap_rpm(clean=options.force) 1458 | generate_katello_facts() 1459 | API_PORT = get_api_port(options.foreman_fqdn) 1460 | RHSM_PORT = get_rhsm_port() 1461 | if 'foreman' not in options.skip: 1462 | create_host() 1463 | configure_subscription_manager() 1464 | migrate_systems(options.org, options.activationkey) 1465 | if options.enablerepos: 1466 | enable_repos() 1467 | elif options.new_capsule: 1468 | # > ELIF new_capsule and foreman_fqdn set, will migrate to other capsule 1469 | # 1470 | # > will replace CA certificate, reinstall katello-agent, gofer 1471 | # > will optionally update hostgroup and location 1472 | # > wil update system definition to point to new capsule for content, 1473 | # > Puppet, OpenSCAP and update Puppet configuration (if applicable) 1474 | # > MANUAL SIGNING OF CSR OR MANUALLY CREATING AUTO-SIGN RULE STILL REQUIRED! 1475 | # > API doesn't have a public endpoint for creating auto-sign entries yet! 1476 | if not is_registered(): 1477 | print_error("This system doesn't seem to be registered to a Capsule at this moment.") 1478 | sys.exit(1) 1479 | 1480 | # Make system ready for switch, gather required data 1481 | install_prereqs() 1482 | get_bootstrap_rpm(clean=True, unreg=False) 1483 | install_katello_host_tools() 1484 | if options.install_katello_agent: 1485 | install_katello_agent() 1486 | if 'katello-agent' in options.skip: 1487 | print_warning("Skipping the installation of the Katello Agent is now the default behavior. passing --skip katello-agent is deprecated") 1488 | enable_rhsmcertd() 1489 | 1490 | API_PORT = get_api_port(options.foreman_fqdn) 1491 | RHSM_PORT = get_rhsm_port() 1492 | if 'foreman' not in options.skip: 1493 | current_host_id = return_matching_foreman_key('hosts', 'name="%s"' % FQDN, 'id', False) 1494 | 1495 | # Optionally configure new hostgroup, location 1496 | if options.hostgroup: 1497 | print_running("Calling Foreman API to switch hostgroup for %s to %s" % (FQDN, options.hostgroup)) 1498 | update_host_config('hostgroup', options.hostgroup, current_host_id) 1499 | if options.location: 1500 | print_running("Calling Foreman API to switch location for %s to %s" % (FQDN, options.location)) 1501 | update_host_config('location', options.location, current_host_id) 1502 | 1503 | # Configure new proxy_id for Puppet (if available and not skipped), and OpenSCAP (if available and not skipped) 1504 | smart_proxy_id = return_matching_foreman_key('smart_proxies', 'name="%s"' % options.foreman_fqdn, 'id', True) 1505 | if smart_proxy_id: 1506 | capsule_features = get_capsule_features(smart_proxy_id) 1507 | if 'puppet' not in options.skip: 1508 | if 'Puppet' in capsule_features: 1509 | print_running("Calling Foreman API to update Puppet server and Puppet CA for %s to %s" % (FQDN, options.foreman_fqdn)) 1510 | update_host_capsule_mapping("puppet_proxy_id", smart_proxy_id, current_host_id) 1511 | update_host_capsule_mapping("puppet_ca_proxy_id", smart_proxy_id, current_host_id) 1512 | else: 1513 | print_warning("New capsule doesn't have Puppet capability, not switching / configuring Puppet server and Puppet CA") 1514 | if 'Openscap' in capsule_features: 1515 | print_running("Calling Foreman API to update OpenSCAP proxy for %s to %s" % (FQDN, options.foreman_fqdn)) 1516 | update_host_capsule_mapping("openscap_proxy_id", smart_proxy_id, current_host_id) 1517 | else: 1518 | print_warning("New capsule doesn't have OpenSCAP capability, not switching / configuring openscap_proxy_id") 1519 | 1520 | print_running("Calling Foreman API to update content source for %s to %s" % (FQDN, options.foreman_fqdn)) 1521 | update_host_capsule_mapping("content_source_id", smart_proxy_id, current_host_id) 1522 | else: 1523 | print_warning("Could not find Smart Proxy '%s'! Will not inform Foreman about the new Puppet/OpenSCAP/content source for %s." % (options.foreman_fqdn, FQDN)) 1524 | 1525 | puppet_conf_path = '/etc/puppetlabs/puppet/puppet.conf' 1526 | if 'puppet' not in options.skip and os.path.exists(puppet_conf_path): 1527 | var_dir = '/opt/puppetlabs/puppet/cache' 1528 | ssl_dir = '/etc/puppetlabs/puppet/ssl' 1529 | 1530 | print_running("Stopping the Puppet agent for configuration update") 1531 | exec_service("puppet", "stop") 1532 | 1533 | # Not using clean_puppet() and install_puppet_agent() here, because 1534 | # that would nuke custom /etc/puppet/puppet.conf files, which might 1535 | # yield undesirable results. 1536 | print_running("Updating Puppet configuration") 1537 | exec_failexit("sed -i '/^[[:space:]]*server.*/ s/=.*/= %s/' %s" % (options.puppet_server, puppet_conf_path)) 1538 | exec_failok("sed -i '/^[[:space:]]*ca_server.*/ s/=.*/= %s/' %s" % (options.puppet_ca_server, puppet_conf_path)) # For RHEL5 stock puppet.conf 1539 | delete_directory(ssl_dir) 1540 | delete_file("%s/client_data/catalog/%s.json" % (var_dir, FQDN)) 1541 | 1542 | noop_puppet_signing_run() 1543 | print_generic("Puppet agent is not running; please start manually if required.") 1544 | print_generic("You also need to manually revoke the certificate on the old capsule.") 1545 | 1546 | else: 1547 | # > ELSE get CA RPM, optionally create host, 1548 | # > register via subscription-manager 1549 | print_generic('This system is not registered to RHN. Attempting to register via subscription-manager') 1550 | install_prereqs() 1551 | get_bootstrap_rpm(clean=options.force) 1552 | generate_katello_facts() 1553 | API_PORT = get_api_port(options.foreman_fqdn) 1554 | RHSM_PORT = get_rhsm_port() 1555 | if 'foreman' not in options.skip: 1556 | create_host() 1557 | configure_subscription_manager() 1558 | if options.preserve_rhsm_proxy: 1559 | set_rhsm_proxy(saved_proxy_config) 1560 | register_systems(options.org, options.activationkey) 1561 | if options.enablerepos: 1562 | enable_repos() 1563 | 1564 | if options.location and 'foreman' in options.skip: 1565 | delete_file('/etc/rhsm/facts/location.facts') 1566 | 1567 | if not options.remove and not options.new_capsule: 1568 | # > IF not removing, install Katello agent, optionally update host, 1569 | # > optionally clean and install Puppet agent 1570 | # > optionally remove legacy RHN packages 1571 | if options.install_katello_agent: 1572 | install_katello_agent() 1573 | if 'katello-agent' in options.skip: 1574 | print_warning("Skipping the installation of the Katello Agent is now the default behavior. passing --skip katello-agent is deprecated") 1575 | if 'katello-host-tools' not in options.skip: 1576 | install_katello_host_tools() 1577 | if options.update: 1578 | fully_update_the_box() 1579 | 1580 | if 'puppet' not in options.skip: 1581 | if options.force: 1582 | clean_puppet() 1583 | install_puppet_agent() 1584 | 1585 | if options.install_packages: 1586 | install_packages() 1587 | 1588 | if 'remove-obsolete-packages' not in options.skip: 1589 | remove_obsolete_packages() 1590 | 1591 | if options.remote_exec: 1592 | if options.remote_exec_proxies: 1593 | listproxies = options.remote_exec_proxies.split(",") 1594 | for proxy_fqdn in listproxies: 1595 | remote_exec_url = "https://" + str(proxy_fqdn) + ":9090/ssh/pubkey" 1596 | install_ssh_key_from_url(remote_exec_url) 1597 | elif options.remote_exec_url: 1598 | install_ssh_key_from_url(options.remote_exec_url) 1599 | elif options.remote_exec_apikeys: 1600 | install_ssh_key_from_api() 1601 | else: 1602 | remote_exec_url = "https://" + str(options.foreman_fqdn) + ":9090/ssh/pubkey" 1603 | install_ssh_key_from_url(remote_exec_url) 1604 | --------------------------------------------------------------------------------