Initialise bootstrapping of repository
Missing tests but most of the copy/pasta is done.
This commit is contained in:
commit
e96a71866d
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Luke Murphy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
73
README.rst
Normal file
73
README.rst
Normal file
@ -0,0 +1,73 @@
|
||||
*****************************
|
||||
Molecule Hetzner Cloud Plugin
|
||||
*****************************
|
||||
|
||||
.. image:: https://badge.fury.io/py/molecule-hetznercloud.svg
|
||||
:target: https://badge.fury.io/py/molecule-hetznercloud
|
||||
:alt: PyPI Package
|
||||
|
||||
.. image:: https://img.shields.io/travis/com/pycontribs/molecule-hetznercloud/master.svg?label=Linux%20builds%20%40%20Travis%20CI
|
||||
:target: https://travis-ci.com/pycontribs/molecule-hetznercloud
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/python/black
|
||||
:alt: Python Black Code Style
|
||||
|
||||
.. image:: https://img.shields.io/badge/Code%20of%20Conduct-Ansible-silver.svg
|
||||
:target: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
|
||||
:alt: Ansible Code of Conduct
|
||||
|
||||
.. image:: https://img.shields.io/badge/Mailing%20lists-Ansible-orange.svg
|
||||
:target: https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information
|
||||
:alt: Ansible mailing lists
|
||||
|
||||
.. image:: https://img.shields.io/badge/license-MIT-brightgreen.svg
|
||||
:target: LICENSE
|
||||
:alt: Repository License
|
||||
|
||||
Molecule Hetzner Cloud driver plugin.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
> https://molecule.readthedocs.io
|
||||
|
||||
.. _get-involved:
|
||||
|
||||
Get Involved
|
||||
============
|
||||
|
||||
* Join us in the ``#ansible-molecule`` channel on `Freenode`_.
|
||||
* Join the discussion in `molecule-users Forum`_.
|
||||
* Join the community working group by checking the `wiki`_.
|
||||
* Want to know about releases, subscribe to `ansible-announce list`_.
|
||||
* For the full list of Ansible email Lists, IRC channels see the
|
||||
`communication page`_.
|
||||
|
||||
.. _`Freenode`: https://freenode.net
|
||||
.. _`molecule-users Forum`: https://groups.google.com/forum/#!forum/molecule-users
|
||||
.. _`wiki`: https://github.com/ansible/community/wiki/Molecule
|
||||
.. _`ansible-announce list`: https://groups.google.com/group/ansible-announce
|
||||
.. _`communication page`: https://docs.ansible.com/ansible/latest/community/communication.html
|
||||
|
||||
.. _authors:
|
||||
|
||||
Authors
|
||||
=======
|
||||
|
||||
Molecule Hetzner Cloud Plugin was created by Luke Murphy based on code from Molecule.
|
||||
|
||||
.. _license:
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The `MIT`_ License.
|
||||
|
||||
.. _`MIT`: https://github.com/ansible/molecule/blob/master/LICENSE
|
||||
|
||||
The logo is licensed under the `Creative Commons NoDerivatives 4.0 License`_.
|
||||
|
||||
If you have some other use in mind, contact us.
|
||||
|
||||
.. _`Creative Commons NoDerivatives 4.0 License`: https://creativecommons.org/licenses/by-nd/4.0/
|
0
molecule_hetzner/__init__.py
Normal file
0
molecule_hetzner/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"molecule_directory": "molecule",
|
||||
"role_name": "OVERRIDDEN",
|
||||
"scenario_name": "OVERRIDDEN"
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
***************************************
|
||||
Hetzner Cloud driver installation guide
|
||||
***************************************
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
* Ansible >= 2.8
|
||||
* ``HCLOUD_TOKEN`` exposed in your environment
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
Please refer to the `Virtual environment`_ documentation for installation best
|
||||
practices. If not using a virtual environment, please consider passing the
|
||||
widely recommended `'--user' flag`_ when invoking ``pip``.
|
||||
|
||||
.. _Virtual environment: https://virtualenv.pypa.io/en/latest/
|
||||
.. _'--user' flag: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install 'molecule[hetznercloud]'
|
@ -0,0 +1,90 @@
|
||||
---
|
||||
{% raw -%}
|
||||
- name: Create
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
no_log: "{{ molecule_no_log }}"
|
||||
vars:
|
||||
ssh_port: 22
|
||||
ssh_user: root
|
||||
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
|
||||
tasks:
|
||||
- name: Create SSH key
|
||||
user:
|
||||
name: "{{ lookup('env', 'USER') }}"
|
||||
generate_ssh_key: true
|
||||
ssh_key_file: "{{ ssh_path }}"
|
||||
force: true
|
||||
register: generated_ssh_key
|
||||
|
||||
- name: Register the SSH key name
|
||||
set_fact:
|
||||
ssh_key_name: "molecule-generated-{{ 12345 | random | to_uuid }}"
|
||||
|
||||
- name: Register SSH key for test instance(s)
|
||||
hcloud_ssh_key:
|
||||
name: "{{ ssh_key_name }}"
|
||||
public_key: "{{ generated_ssh_key.ssh_public_key }}"
|
||||
state: present
|
||||
|
||||
- name: Create molecule instance(s)
|
||||
hcloud_server:
|
||||
name: "{{ item.name }}"
|
||||
server_type: "{{ item.server_type }}"
|
||||
ssh_keys:
|
||||
- "{{ ssh_key_name }}"
|
||||
volumes: "{{ item.volumes | default(omit) }}"
|
||||
image: "{{ item.image }}"
|
||||
location: "{{ item.location | default(omit) }}"
|
||||
datacenter: "{{ item.datacenter | default(omit) }}"
|
||||
user_data: "{{ item.user_data | default(omit) }}"
|
||||
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||
state: present
|
||||
register: server
|
||||
with_items: "{{ molecule_yml.platforms }}"
|
||||
async: 7200
|
||||
poll: 0
|
||||
|
||||
- name: Wait for instance(s) creation to complete
|
||||
async_status:
|
||||
jid: "{{ item.ansible_job_id }}"
|
||||
register: hetzner_jobs
|
||||
until: hetzner_jobs.finished
|
||||
retries: 300
|
||||
with_items: "{{ server.results }}"
|
||||
|
||||
# Mandatory configuration for Molecule to function.
|
||||
|
||||
- name: Populate instance config dict
|
||||
set_fact:
|
||||
instance_conf_dict: {
|
||||
'instance': "{{ item.hcloud_server.name }}",
|
||||
'ssh_key_name': "{{ ssh_key_name }}",
|
||||
'address': "{{ item.hcloud_server.ipv4_address }}",
|
||||
'user': "{{ ssh_user }}",
|
||||
'port': "{{ ssh_port }}",
|
||||
'identity_file': "{{ ssh_path }}", }
|
||||
with_items: "{{ hetzner_jobs.results }}"
|
||||
register: instance_config_dict
|
||||
when: server.changed | bool
|
||||
|
||||
- name: Convert instance config dict to a list
|
||||
set_fact:
|
||||
instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
|
||||
when: server.changed | bool
|
||||
|
||||
- name: Dump instance config
|
||||
copy:
|
||||
content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}"
|
||||
dest: "{{ molecule_instance_config }}"
|
||||
when: server.changed | bool
|
||||
|
||||
- name: Wait for SSH
|
||||
wait_for:
|
||||
port: "{{ ssh_port }}"
|
||||
host: "{{ item.address }}"
|
||||
search_regex: SSH
|
||||
delay: 10
|
||||
with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
|
||||
{%- endraw %}
|
@ -0,0 +1,59 @@
|
||||
---
|
||||
{% raw -%}
|
||||
- name: Destroy
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
no_log: "{{ molecule_no_log }}"
|
||||
tasks:
|
||||
- name: Populate the instance config
|
||||
block:
|
||||
- name: Populate instance config from file
|
||||
set_fact:
|
||||
instance_conf: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
|
||||
skip_instances: false
|
||||
rescue:
|
||||
- name: Populate instance config when file missing
|
||||
set_fact:
|
||||
instance_conf: {}
|
||||
skip_instances: true
|
||||
|
||||
- name: Destroy molecule instance(s)
|
||||
hcloud_server:
|
||||
name: "{{ item.instance }}"
|
||||
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||
state: absent
|
||||
register: server
|
||||
with_items: "{{ instance_conf }}"
|
||||
when: not skip_instances
|
||||
async: 7200
|
||||
poll: 0
|
||||
|
||||
- name: Wait for instance(s) deletion to complete
|
||||
async_status:
|
||||
jid: "{{ item.ansible_job_id }}"
|
||||
register: hetzner_jobs
|
||||
until: hetzner_jobs.finished
|
||||
retries: 300
|
||||
with_items: "{{ server.results }}"
|
||||
|
||||
- name: Remove registered SSH key
|
||||
hcloud_ssh_key:
|
||||
name: "{{ instance_conf[0].ssh_key_name }}"
|
||||
state: absent
|
||||
when:
|
||||
- not skip_instances
|
||||
- instance_conf # must contain at least one instance
|
||||
|
||||
# Mandatory configuration for Molecule to function.
|
||||
|
||||
- name: Populate instance config
|
||||
set_fact:
|
||||
instance_conf: {}
|
||||
|
||||
- name: Dump instance config
|
||||
copy:
|
||||
content: "{{ instance_conf | molecule_to_yaml | molecule_header }}"
|
||||
dest: "{{ molecule_instance_config }}"
|
||||
when: server.changed | bool
|
||||
{%- endraw %}
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Converge
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: "Include {{ cookiecutter.role_name }}"
|
||||
include_role:
|
||||
name: "{{ cookiecutter.role_name }}"
|
@ -0,0 +1,11 @@
|
||||
---
|
||||
{% raw -%}
|
||||
- name: Prepare
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Install python for Ansible
|
||||
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-zipstream)
|
||||
become: true
|
||||
changed_when: false
|
||||
{%- endraw %}
|
174
molecule_hetzner/hetznercloud.py
Normal file
174
molecule_hetzner/hetznercloud.py
Normal file
@ -0,0 +1,174 @@
|
||||
# Copyright (c) 2019 Red Hat, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import os
|
||||
|
||||
from molecule import logger, util
|
||||
from molecule.api import Driver
|
||||
from molecule.util import lru_cache
|
||||
from molecule.util import sysexit_with_message
|
||||
|
||||
log = logger.get_logger(__name__)
|
||||
|
||||
|
||||
class HetznerCloud(Driver):
|
||||
"""
|
||||
The class responsible for managing `Hetzner Cloud`_ instances.
|
||||
`Hetzner Cloud`_ is **not** the default driver used in Molecule.
|
||||
|
||||
Molecule leverages Ansible's `hcloud_server module`_, by mapping variables
|
||||
from ``molecule.yml`` into ``create.yml`` and ``destroy.yml``.
|
||||
|
||||
.. important::
|
||||
|
||||
The ``hcloud_server`` module is only available in Ansible >= 2.8.
|
||||
|
||||
.. _`hcloud_server module`: https://docs.ansible.com/ansible/devel/modules/hcloud_server_module.html#hcloud-server-module
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
driver:
|
||||
name: hetznercloud
|
||||
platforms:
|
||||
- name: instance
|
||||
server_type: cx11
|
||||
image: debian-9
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install 'molecule[hetznercloud]'
|
||||
|
||||
Change the options passed to the ssh client.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
driver:
|
||||
name: hetznercloud
|
||||
ssh_connection_options:
|
||||
- '-o ControlPath=~/.ansible/cp/%r@%h-%p'
|
||||
|
||||
.. important::
|
||||
|
||||
The Hetzner Cloud driver implementation uses the Parmiko transport
|
||||
provided by Ansible to avoid issues of connection hanging and
|
||||
indefinite polling as experienced with the default OpenSSh based
|
||||
transport.
|
||||
|
||||
.. important::
|
||||
|
||||
Molecule does not merge lists, when overriding the developer must
|
||||
provide all options.
|
||||
|
||||
Provide the files Molecule will preserve upon each subcommand execution.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
driver:
|
||||
name: hetznercloud
|
||||
safe_files:
|
||||
- foo
|
||||
""" # noqa
|
||||
|
||||
def __init__(self, config=None):
|
||||
super(HetznerCloud, self).__init__(config)
|
||||
self._name = 'hetznercloud'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def login_cmd_template(self):
|
||||
connection_options = ' '.join(self.ssh_connection_options)
|
||||
|
||||
return (
|
||||
'ssh {{address}} '
|
||||
'-l {{user}} '
|
||||
'-p {{port}} '
|
||||
'-i {{identity_file}} '
|
||||
'{}'
|
||||
).format(connection_options)
|
||||
|
||||
@property
|
||||
def default_safe_files(self):
|
||||
return [self.instance_config]
|
||||
|
||||
@property
|
||||
def default_ssh_connection_options(self):
|
||||
return self._get_ssh_connection_options()
|
||||
|
||||
def login_options(self, instance_name):
|
||||
d = {'instance': instance_name}
|
||||
|
||||
return util.merge_dicts(d, self._get_instance_config(instance_name))
|
||||
|
||||
def ansible_connection_options(self, instance_name):
|
||||
try:
|
||||
d = self._get_instance_config(instance_name)
|
||||
|
||||
return {
|
||||
'ansible_user': d['user'],
|
||||
'ansible_host': d['address'],
|
||||
'ansible_port': d['port'],
|
||||
'ansible_private_key_file': d['identity_file'],
|
||||
'connection': 'ssh',
|
||||
'ansible_ssh_common_args': ' '.join(self.ssh_connection_options),
|
||||
}
|
||||
except StopIteration:
|
||||
return {}
|
||||
except IOError:
|
||||
# Instance has yet to be provisioned , therefore the
|
||||
# instance_config is not on disk.
|
||||
return {}
|
||||
|
||||
def _get_instance_config(self, instance_name):
|
||||
instance_config_dict = util.safe_load_file(self._config.driver.instance_config)
|
||||
|
||||
return next(
|
||||
item for item in instance_config_dict if item['instance'] == instance_name
|
||||
)
|
||||
|
||||
@lru_cache()
|
||||
def sanity_checks(self):
|
||||
"""Hetzner Cloud driver sanity checks."""
|
||||
|
||||
log.info("Sanity checks: '{}'".format(self._name))
|
||||
|
||||
try:
|
||||
import hcloud # noqa
|
||||
except ImportError:
|
||||
msg = (
|
||||
'Missing Hetzner Cloud driver dependency. Please '
|
||||
"install via 'molecule[hetznercloud]' or refer to "
|
||||
'your INSTALL.rst driver documentation file'
|
||||
)
|
||||
sysexit_with_message(msg)
|
||||
|
||||
if 'HCLOUD_TOKEN' not in os.environ:
|
||||
msg = (
|
||||
'Missing Hetzner Cloud API token. Please expose '
|
||||
'the HCLOUD_TOKEN environment variable with your '
|
||||
'account API token value'
|
||||
)
|
||||
sysexit_with_message(msg)
|
11
pyproject.toml
Normal file
11
pyproject.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools >= 41.0.0",
|
||||
"setuptools_scm >= 1.15.0",
|
||||
"setuptools_scm_git_archive >= 1.0",
|
||||
"wheel",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
skip-string-normalization = true
|
12
pytest.ini
Normal file
12
pytest.ini
Normal file
@ -0,0 +1,12 @@
|
||||
[pytest]
|
||||
addopts = -v -rxXs --doctest-modules --durations 10 --cov=molecule --cov-report term-missing:skip-covered --cov-report xml -m "not molecule"
|
||||
doctest_optionflags = ALLOW_UNICODE ELLIPSIS
|
||||
junit_suite_name = molecule_test_suite
|
||||
norecursedirs = dist doc build .tox .eggs
|
||||
filterwarnings =
|
||||
# remove once https://github.com/cookiecutter/cookiecutter/pull/1127 is released
|
||||
ignore::DeprecationWarning:cookiecutter
|
||||
# remove once https://github.com/pytest-dev/pytest-cov/issues/327 is released
|
||||
ignore::pytest.PytestWarning:pytest_cov
|
||||
# remove once https://bitbucket.org/astanin/python-tabulate/issues/174/invalid-escape-sequence
|
||||
ignore::DeprecationWarning:tabulate
|
88
setup.cfg
Normal file
88
setup.cfg
Normal file
@ -0,0 +1,88 @@
|
||||
[aliases]
|
||||
dists = clean --all sdist bdist_wheel
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
name = molecule-hetznercloud
|
||||
url = https://github.com/pycontribs/molecule-hetznercloud
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/pycontribs/molecule-hetznercloud/issues
|
||||
Release Management = https://github.com/pycontribs/molecule-hetznercloud/projects
|
||||
CI: Travis = https://travis-ci.com/pycontribs/molecule-hetznercloud
|
||||
Source Code = https://github.com/pycontribs/molecule-hetznercloud
|
||||
description = Hetznercloud Molecule Plugin :: run molecule tests with hetznercloud as verifier
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
author = Luke Murphy
|
||||
author_email = lukewm@riseup.net
|
||||
maintainer = Luke Murphy
|
||||
maintainer_email = lukewm@riseup.net
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
classifiers =
|
||||
Development Status :: 5 - Production/Stable
|
||||
|
||||
Environment :: Console
|
||||
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
|
||||
License :: OSI Approved :: MIT License
|
||||
|
||||
Natural Language :: English
|
||||
|
||||
Operating System :: OS Independent
|
||||
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
|
||||
Topic :: System :: Systems Administration
|
||||
Topic :: Utilities
|
||||
keywords =
|
||||
ansible
|
||||
roles
|
||||
testing
|
||||
molecule
|
||||
plugin
|
||||
hetznercloud
|
||||
verifier
|
||||
|
||||
[options]
|
||||
use_scm_version = True
|
||||
python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
|
||||
packages = find:
|
||||
include_package_data = True
|
||||
zip_safe = False
|
||||
setup_requires =
|
||||
setuptools_scm >= 1.15.0
|
||||
setuptools_scm_git_archive >= 1.0
|
||||
install_requires =
|
||||
molecule >= 3.0a3
|
||||
pyyaml >= 5.1, < 6
|
||||
backports.shutil_which ; python_version<"3.3"
|
||||
|
||||
[options.extras_require]
|
||||
test =
|
||||
flake8>=3.6.0, < 4
|
||||
mock>=3.0.5, < 4
|
||||
pytest>=4.6.3, < 5
|
||||
pytest-cov>=2.7.1, < 3
|
||||
pytest-helpers-namespace>=2019.1.8, < 2020
|
||||
pytest-mock>=1.10.4, < 2
|
||||
pytest-verbose-parametrize>=1.7.0, < 2
|
||||
pytest-xdist>=1.29.0, < 2
|
||||
hcloud>=1.2.1
|
||||
|
||||
[options.entry_points]
|
||||
molecule.verifier =
|
||||
hetznercloud = molecule_hetznercloud.hetznercloud:hetznercloud
|
||||
|
||||
[options.packages.find]
|
||||
where = .
|
330
setup.py
Normal file
330
setup.py
Normal file
@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import setuptools
|
||||
|
||||
HAS_DIST_INFO_CMD = False
|
||||
try:
|
||||
import setuptools.command.dist_info
|
||||
|
||||
HAS_DIST_INFO_CMD = True
|
||||
except ImportError:
|
||||
"""Setuptools version is too old."""
|
||||
|
||||
|
||||
ALL_STRING_TYPES = tuple(map(type, ("", b"", u"")))
|
||||
MIN_NATIVE_SETUPTOOLS_VERSION = 34, 4, 0
|
||||
"""Minimal setuptools having good read_configuration implementation."""
|
||||
|
||||
RUNTIME_SETUPTOOLS_VERSION = tuple(map(int, setuptools.__version__.split(".")))
|
||||
"""Setuptools imported now."""
|
||||
|
||||
READ_CONFIG_SHIM_NEEDED = RUNTIME_SETUPTOOLS_VERSION < MIN_NATIVE_SETUPTOOLS_VERSION
|
||||
|
||||
|
||||
def str_if_nested_or_str(s):
|
||||
"""Turn input into a native string if possible."""
|
||||
if isinstance(s, ALL_STRING_TYPES):
|
||||
return str(s)
|
||||
if isinstance(s, (list, tuple)):
|
||||
return type(s)(map(str_if_nested_or_str, s))
|
||||
if isinstance(s, (dict,)):
|
||||
return stringify_dict_contents(s)
|
||||
return s
|
||||
|
||||
|
||||
def stringify_dict_contents(dct):
|
||||
"""Turn dict keys and values into native strings."""
|
||||
return {str_if_nested_or_str(k): str_if_nested_or_str(v) for k, v in dct.items()}
|
||||
|
||||
|
||||
if not READ_CONFIG_SHIM_NEEDED:
|
||||
from setuptools.config import read_configuration, ConfigOptionsHandler
|
||||
import setuptools.config
|
||||
import setuptools.dist
|
||||
|
||||
# Set default value for 'use_scm_version'
|
||||
setattr(setuptools.dist.Distribution, "use_scm_version", False)
|
||||
|
||||
# Attach bool parser to 'use_scm_version' option
|
||||
class ShimConfigOptionsHandler(ConfigOptionsHandler):
|
||||
"""Extension class for ConfigOptionsHandler."""
|
||||
|
||||
@property
|
||||
def parsers(self):
|
||||
"""Return an option mapping with default data type parsers."""
|
||||
_orig_parsers = super(ShimConfigOptionsHandler, self).parsers
|
||||
return dict(use_scm_version=self._parse_bool, **_orig_parsers)
|
||||
|
||||
def parse_section_packages__find(self, section_options):
|
||||
find_kwargs = super(
|
||||
ShimConfigOptionsHandler, self
|
||||
).parse_section_packages__find(section_options)
|
||||
return stringify_dict_contents(find_kwargs)
|
||||
|
||||
setuptools.config.ConfigOptionsHandler = ShimConfigOptionsHandler
|
||||
else:
|
||||
"""This is a shim for setuptools<required."""
|
||||
import functools
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import setuptools.config
|
||||
|
||||
def filter_out_unknown_section(i):
|
||||
def chi(self, *args, **kwargs):
|
||||
i(self, *args, **kwargs)
|
||||
self.sections = {
|
||||
s: v for s, v in self.sections.items() if s != "packages.find"
|
||||
}
|
||||
|
||||
return chi
|
||||
|
||||
setuptools.config.ConfigHandler.__init__ = filter_out_unknown_section(
|
||||
setuptools.config.ConfigHandler.__init__
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def ignore_unknown_options(s):
|
||||
@functools.wraps(s)
|
||||
def sw(**attrs):
|
||||
try:
|
||||
ignore_warning_regex = (
|
||||
r"Unknown distribution option: "
|
||||
r"'(license_file|project_urls|python_requires)'"
|
||||
)
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=ignore_warning_regex,
|
||||
category=UserWarning,
|
||||
module="distutils.dist",
|
||||
)
|
||||
return s(**attrs)
|
||||
finally:
|
||||
warnings.resetwarnings()
|
||||
|
||||
return sw
|
||||
|
||||
def parse_predicates(python_requires):
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
sorted_operators_map = tuple(
|
||||
sorted(
|
||||
{
|
||||
">": operator.gt,
|
||||
"<": operator.lt,
|
||||
">=": operator.ge,
|
||||
"<=": operator.le,
|
||||
"==": operator.eq,
|
||||
"!=": operator.ne,
|
||||
"": operator.eq,
|
||||
}.items(),
|
||||
key=lambda i: len(i[0]),
|
||||
reverse=True,
|
||||
)
|
||||
)
|
||||
|
||||
def is_decimal(s):
|
||||
return type(u"")(s).isdecimal()
|
||||
|
||||
conditions = map(str.strip, python_requires.split(","))
|
||||
for c in conditions:
|
||||
for op_sign, op_func in sorted_operators_map:
|
||||
if not c.startswith(op_sign):
|
||||
continue
|
||||
raw_ver = itertools.takewhile(
|
||||
is_decimal, c[len(op_sign) :].strip().split(".")
|
||||
)
|
||||
ver = tuple(map(int, raw_ver))
|
||||
yield op_func, ver
|
||||
break
|
||||
|
||||
def validate_required_python_or_fail(python_requires=None):
|
||||
if python_requires is None:
|
||||
return
|
||||
|
||||
python_version = sys.version_info
|
||||
preds = parse_predicates(python_requires)
|
||||
for op, v in preds:
|
||||
py_ver_slug = python_version[: max(len(v), 3)]
|
||||
condition_matches = op(py_ver_slug, v)
|
||||
if not condition_matches:
|
||||
raise RuntimeError(
|
||||
"requires Python '{}' but the running Python is {}".format(
|
||||
python_requires, ".".join(map(str, python_version[:3]))
|
||||
)
|
||||
)
|
||||
|
||||
def verify_required_python_runtime(s):
|
||||
@functools.wraps(s)
|
||||
def sw(**attrs):
|
||||
try:
|
||||
validate_required_python_or_fail(attrs.get("python_requires"))
|
||||
except RuntimeError as re:
|
||||
sys.exit("{} {!s}".format(attrs["name"], re))
|
||||
return s(**attrs)
|
||||
|
||||
return sw
|
||||
|
||||
setuptools.setup = ignore_unknown_options(setuptools.setup)
|
||||
setuptools.setup = verify_required_python_runtime(setuptools.setup)
|
||||
|
||||
try:
|
||||
from configparser import ConfigParser, NoSectionError
|
||||
except ImportError:
|
||||
from ConfigParser import ConfigParser, NoSectionError
|
||||
|
||||
ConfigParser.read_file = ConfigParser.readfp
|
||||
|
||||
def maybe_read_files(d):
|
||||
"""Read files if the string starts with `file:` marker."""
|
||||
FILE_FUNC_MARKER = "file:"
|
||||
|
||||
d = d.strip()
|
||||
if not d.startswith(FILE_FUNC_MARKER):
|
||||
return d
|
||||
descs = []
|
||||
for fname in map(str.strip, str(d[len(FILE_FUNC_MARKER) :]).split(",")):
|
||||
with io.open(fname, encoding="utf-8") as f:
|
||||
descs.append(f.read())
|
||||
return "".join(descs)
|
||||
|
||||
def cfg_val_to_list(v):
|
||||
"""Turn config val to list and filter out empty lines."""
|
||||
return list(filter(bool, map(str.strip, str(v).strip().splitlines())))
|
||||
|
||||
def cfg_val_to_dict(v):
|
||||
"""Turn config val to dict and filter out empty lines."""
|
||||
return dict(
|
||||
map(
|
||||
lambda l: list(map(str.strip, l.split("=", 1))),
|
||||
filter(bool, map(str.strip, str(v).strip().splitlines())),
|
||||
)
|
||||
)
|
||||
|
||||
def cfg_val_to_primitive(v):
|
||||
"""Parse primitive config val to appropriate data type."""
|
||||
return json.loads(v.strip().lower())
|
||||
|
||||
def read_configuration(filepath):
|
||||
"""Read metadata and options from setup.cfg located at filepath."""
|
||||
cfg = ConfigParser()
|
||||
with io.open(filepath, encoding="utf-8") as f:
|
||||
cfg.read_file(f)
|
||||
|
||||
md = dict(cfg.items("metadata"))
|
||||
for list_key in "classifiers", "keywords", "project_urls":
|
||||
try:
|
||||
md[list_key] = cfg_val_to_list(md[list_key])
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
md["long_description"] = maybe_read_files(md["long_description"])
|
||||
except KeyError:
|
||||
pass
|
||||
opt = dict(cfg.items("options"))
|
||||
for list_key in "include_package_data", "use_scm_version", "zip_safe":
|
||||
try:
|
||||
opt[list_key] = cfg_val_to_primitive(opt[list_key])
|
||||
except KeyError:
|
||||
pass
|
||||
for list_key in "scripts", "install_requires", "setup_requires":
|
||||
try:
|
||||
opt[list_key] = cfg_val_to_list(opt[list_key])
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
opt["package_dir"] = cfg_val_to_dict(opt["package_dir"])
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
opt_package_data = dict(cfg.items("options.package_data"))
|
||||
if not opt_package_data.get("", "").strip():
|
||||
opt_package_data[""] = opt_package_data["*"]
|
||||
del opt_package_data["*"]
|
||||
except (KeyError, NoSectionError):
|
||||
opt_package_data = {}
|
||||
try:
|
||||
opt_extras_require = dict(cfg.items("options.extras_require"))
|
||||
opt["extras_require"] = {}
|
||||
for k, v in opt_extras_require.items():
|
||||
opt["extras_require"][k] = cfg_val_to_list(v)
|
||||
except NoSectionError:
|
||||
pass
|
||||
opt["package_data"] = {}
|
||||
for k, v in opt_package_data.items():
|
||||
opt["package_data"][k] = cfg_val_to_list(v)
|
||||
try:
|
||||
opt_exclude_package_data = dict(cfg.items("options.exclude_package_data"))
|
||||
if (
|
||||
not opt_exclude_package_data.get("", "").strip()
|
||||
and "*" in opt_exclude_package_data
|
||||
):
|
||||
opt_exclude_package_data[""] = opt_exclude_package_data["*"]
|
||||
del opt_exclude_package_data["*"]
|
||||
except NoSectionError:
|
||||
pass
|
||||
else:
|
||||
opt["exclude_package_data"] = {}
|
||||
for k, v in opt_exclude_package_data.items():
|
||||
opt["exclude_package_data"][k] = cfg_val_to_list(v)
|
||||
cur_pkgs = opt.get("packages", "").strip()
|
||||
if "\n" in cur_pkgs:
|
||||
opt["packages"] = cfg_val_to_list(opt["packages"])
|
||||
elif cur_pkgs.startswith("find:"):
|
||||
opt_packages_find = stringify_dict_contents(
|
||||
dict(cfg.items("options.packages.find"))
|
||||
)
|
||||
opt["packages"] = setuptools.find_packages(**opt_packages_find)
|
||||
return {"metadata": md, "options": opt}
|
||||
|
||||
|
||||
def cut_local_version_on_upload(version):
|
||||
"""Generate a PEP440 local version if uploading to PyPI."""
|
||||
import os
|
||||
import setuptools_scm.version # only present during setup time
|
||||
|
||||
IS_PYPI_UPLOAD = os.getenv("PYPI_UPLOAD") == "true" # set in tox.ini
|
||||
return (
|
||||
""
|
||||
if IS_PYPI_UPLOAD
|
||||
else setuptools_scm.version.get_local_node_and_date(version)
|
||||
)
|
||||
|
||||
|
||||
if HAS_DIST_INFO_CMD:
|
||||
|
||||
class patched_dist_info(setuptools.command.dist_info.dist_info):
|
||||
def run(self):
|
||||
self.egg_base = str_if_nested_or_str(self.egg_base)
|
||||
return setuptools.command.dist_info.dist_info.run(self)
|
||||
|
||||
|
||||
declarative_setup_params = read_configuration("setup.cfg")
|
||||
"""Declarative metadata and options as read by setuptools."""
|
||||
|
||||
|
||||
setup_params = {}
|
||||
"""Explicit metadata for passing into setuptools.setup() call."""
|
||||
|
||||
setup_params = dict(setup_params, **declarative_setup_params["metadata"])
|
||||
setup_params = dict(setup_params, **declarative_setup_params["options"])
|
||||
|
||||
if HAS_DIST_INFO_CMD:
|
||||
setup_params["cmdclass"] = {"dist_info": patched_dist_info}
|
||||
|
||||
setup_params["use_scm_version"] = {"local_scheme": cut_local_version_on_upload}
|
||||
|
||||
# Patch incorrectly decoded package_dir option
|
||||
# ``egg_info`` demands native strings failing with unicode under Python 2
|
||||
# Ref https://github.com/pypa/setuptools/issues/1136
|
||||
setup_params = stringify_dict_contents(setup_params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setuptools.setup(**setup_params)
|
102
tox.ini
Normal file
102
tox.ini
Normal file
@ -0,0 +1,102 @@
|
||||
[tox]
|
||||
minversion = 3.12.0
|
||||
envlist =
|
||||
lint
|
||||
check
|
||||
py{27,35,36,37}-ansible{27,28,29}
|
||||
doc
|
||||
devel
|
||||
skipdist = True
|
||||
skip_missing_interpreters = True
|
||||
isolated_build = True
|
||||
|
||||
[testenv]
|
||||
# Hotfix for https://github.com/pypa/pip/issues/6434
|
||||
# Resolved by https://github.com/tox-dev/tox/issues/1276
|
||||
install_command =
|
||||
python -c 'import subprocess, sys; pip_inst_cmd = sys.executable, "-m", "pip", "install"; subprocess.check_call(pip_inst_cmd + ("pip<19.1", )); subprocess.check_call(pip_inst_cmd + tuple(sys.argv[1:]))' {opts} {packages}
|
||||
usedevelop = True
|
||||
passenv = *
|
||||
setenv =
|
||||
ANSIBLE_CALLABLE_WHITELIST={env:ANSIBLE_CALLABLE_WHITELIST:timer,profile_roles}
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
ansible27: ansible>=2.7,<2.8
|
||||
ansible28: ansible>=2.8,<2.9
|
||||
ansible29: ansible>=2.9.0b1,<2.10
|
||||
devel: ansible>=2.9.0b1
|
||||
devel: docker
|
||||
extras =
|
||||
test
|
||||
commands =
|
||||
pip check
|
||||
devel: pip install -e "git+https://github.com/ansible/molecule.git#egg=molecule"
|
||||
python -m pytest {posargs}
|
||||
whitelist_externals =
|
||||
find
|
||||
sh
|
||||
|
||||
[testenv:lint]
|
||||
commands =
|
||||
# to run a single linter you can do "pre-commit run flake8"
|
||||
python -m pre_commit run {posargs:--all}
|
||||
deps = pre-commit>=1.18.1
|
||||
extras =
|
||||
skip_install = true
|
||||
usedevelop = false
|
||||
|
||||
[testenv:check]
|
||||
envdir = {toxworkdir}/py36-ansible28
|
||||
deps =
|
||||
{[testenv]deps}
|
||||
collective.checkdocs==0.2
|
||||
twine==1.14.0
|
||||
usedevelop = False
|
||||
commands =
|
||||
python -m pytest --collect-only
|
||||
python -m setup checkdocs check --metadata --restructuredtext --strict --verbose
|
||||
python -m twine check .tox/dist/*
|
||||
|
||||
[testenv:build-docker]
|
||||
platform = ^darwin|^linux
|
||||
usedevelop = False
|
||||
skip_install = True
|
||||
deps =
|
||||
setuptools_scm==3.3.3
|
||||
packaging # pyup: ignore
|
||||
commands_pre =
|
||||
commands =
|
||||
python ./utils/build-docker.py
|
||||
whitelist_externals =
|
||||
sh
|
||||
|
||||
[testenv:build-dists-local]
|
||||
description =
|
||||
Generate dists which may be not ready
|
||||
for upload to PyPI because of
|
||||
containing PEP440 local version part
|
||||
usedevelop = false
|
||||
skip_install = true
|
||||
deps =
|
||||
pep517 >= 0.5.0
|
||||
setenv =
|
||||
commands =
|
||||
python -m pep517.build \
|
||||
--source \
|
||||
--binary \
|
||||
--out-dir {toxinidir}/dist/ \
|
||||
{toxinidir}
|
||||
|
||||
[testenv:build-dists]
|
||||
description = Generate dists ready for upload to PyPI
|
||||
usedevelop = {[testenv:build-dists-local]usedevelop}
|
||||
skip_install = {[testenv:build-dists-local]skip_install}
|
||||
deps = {[testenv:build-dists-local]deps}
|
||||
setenv =
|
||||
PYPI_UPLOAD = true
|
||||
commands =
|
||||
rm -rfv {toxinidir}/dist/
|
||||
{[testenv:build-dists-local]commands}
|
||||
whitelist_externals =
|
||||
rm
|
||||
{[testenv]whitelist_externals}
|
Loading…
Reference in New Issue
Block a user