diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4e6587c..aaf286f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v3 with: - python-version: '3.8' + python-version: "3.8" - name: Upgrade pip run: | diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index a69cac0..44aedca 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: GitHub Action for Flake8 - uses: cclauss/GitHub-Action-for-Flake8@v0.5.0 + - uses: actions/checkout@v3 + - name: GitHub Action for Flake8 + uses: cclauss/GitHub-Action-for-Flake8@v0.5.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bcca791..0069b8b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,25 +3,25 @@ name: Upload Python Package on: push: tags: - - 'v*' + - "v*" jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -U setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a00ce0..56afde5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,22 +6,21 @@ jobs: build: runs-on: ubuntu-latest strategy: - max-parallel: 4 + max-parallel: 3 matrix: python-version: - - "3.6" - - "3.7" - "3.8" + - "3.10" steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/docs/cloud.md b/docs/cloud.md index dcba1fb..0937bc5 100644 --- a/docs/cloud.md +++ b/docs/cloud.md @@ -122,5 +122,42 @@ cloud: # ddos_protection: true # firewall_group_id: ... # enable_private_network: true +``` + +## Apache CloudStack + +```yaml + kind: cloudstack + launch_config: + service_offering: cpu2-ram2 + template: Linux Template xyz + zone: my-zone + ssh_key: my_ssh_key + tags: + project: gemini + root_disk_size: 20 + user_data: | + #cloud-config + manage_etc_hosts: true + packages: + - nginx +``` +## Exoscale + +```yaml + kind: exoscale + launch_config: + service_offering: Micro + template: Linux Debian 11 (Bullseye) 64-bit + zone: ch-dk-2 + ssh_key: my-ssh-key + tags: + project: gemini + root_disk_size: 20 + user_data: | + #cloud-config + manage_etc_hosts: true + packages: + - nginx ``` diff --git a/requirements.txt b/requirements.txt index 449977e..bce9758 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -cloudscale-sdk==0.6.2 +cloudscale-sdk==0.7.0 cs==3.0.0 hcloud==1.16.0 prometheus-api-client==0.5.0 prometheus-client==0.13.1 -pydantic==1.9.0 +pydantic-yaml==0.6.3 +pydantic==1.9.1 python-digitalocean==1.17.0 python-dotenv==0.19.2 python-json-logger==2.0.2 PyYAML==6.0 requests==2.27.1 schedule==1.1.0 -pydantic-yaml==0.6.3 diff --git a/scalr/cloud/adapters/cloudscale_ch.py b/scalr/cloud/adapters/cloudscale_ch.py index 271eacc..5c9c81a 100644 --- a/scalr/cloud/adapters/cloudscale_ch.py +++ b/scalr/cloud/adapters/cloudscale_ch.py @@ -13,7 +13,7 @@ def __init__(self): def get_current_instances(self) -> List[GenericCloudInstance]: filter_tag = f"scalr={self.filter}" log.info(f"cloudscale: Querying with filter_tag: {filter_tag}") - servers = self.cloudscale.server.get_all(filter_tag=filter_tag) # type: ignore + servers = self.cloudscale.server.get_all(filter_tag=filter_tag) return [ GenericCloudInstance( id=server["uuid"], @@ -28,7 +28,7 @@ def ensure_instances_running(self) -> None: for instance in self.get_current_instances(): log.info(f"cloudscale: instance, {instance.name} status {instance.status}") if instance.status == "stopped": - self.cloudscale.server.start(uuid=instance.id) # type: ignore + self.cloudscale.server.start(uuid=instance.id) log.info(f"cloudscale: Instance {instance.name} started") def deploy_instance(self, name: str) -> None: @@ -44,8 +44,8 @@ def deploy_instance(self, name: str) -> None: "tags": tags, } ) - self.cloudscale.server.create(**launch_config) # type: ignore + self.cloudscale.server.create(**launch_config) def destroy_instance(self, instance: GenericCloudInstance) -> None: log.info(f"cloudscale: Destroying instance {instance}") - self.cloudscale.server.delete(uuid=instance.id) # type: ignore + self.cloudscale.server.delete(uuid=instance.id) diff --git a/scalr/cloud/adapters/cloudstack.py b/scalr/cloud/adapters/cloudstack.py index 74dd09d..a82c88b 100644 --- a/scalr/cloud/adapters/cloudstack.py +++ b/scalr/cloud/adapters/cloudstack.py @@ -16,76 +16,19 @@ def __init__(self): secret=os.getenv("CLOUDSTACK_API_SECRET"), ) - def get_current_instances(self) -> List[GenericCloudInstance]: - filter_tag = f"scalr={self.filter}" - log.info(f"cloudstack: Querying with filter_tag: {filter_tag}") - servers = self.cs.listVirtualMachines( - tags=[ - { - "key": "scalr", - "value": self.filter, - } - ], - fetch_list=True, - ) - return [ - GenericCloudInstance( - id=server["id"], - name=server["name"], - status=server["status"], - ) - for server in sorted(servers, key=lambda i: i["created"]) - ] - - - def ensure_instances_running(self): - - def deploy_instance(self, name: str): - - def destroy_instance(self, instance: GenericCloudInstance): - - - -class CloudstackCloudAdapter2(CloudAdapter): - - - def get_current(self) -> list: - if self.current_servers is None: - servers = self.cs.listVirtualMachines( - tags=[ - { - "key": "scalr", - "value": self.name, - } - ], - fetch_list=True, - ) - self.current_servers = sorted(servers, key=lambda i: i["created"]) - return self.current_servers - - def ensure_running(self): - for server in self.get_current(): - log.info(f"server {server['name']} status {server['state']}") - if server["state"] in ["stopping", "stopped"]: - if not self.dry_run: - self.cs.startVirtualMachine(id=server["id"]) - log.info(f"Server {server['name']} started") - else: - log.info(f"Dry run server {server['name']} started") - - def _get_service_offering(self, name): + def get_service_offering(self, name) -> dict: res = self.cs.listServiceOfferings(name=name) if not res: raise Exception(f"Error: Service offering not found: {name}") return res["serviceoffering"][0] - def _get_zone(self, name): + def get_zone(self, name) -> dict: res = self.cs.listZones(name=name) if not res: raise Exception(f"Error: Zone not found: {name}") return res["zone"][0] - def _get_template(self, name): + def get_template(self, name) -> dict: for tf in ["community", "self"]: res = self.cs.listTemplates(name=name, templatefilter=tf) if res: @@ -94,72 +37,73 @@ def _get_template(self, name): raise Exception(f"Error: Template not found: {name}") return res["template"][0] - def _get_deploy_params(self, lc): - user_data = lc.get("user_data") + def get_params(self, name: str) -> dict: + user_data = self.launch.get("user_data") if user_data: user_data = base64.b64encode(user_data.encode("utf-8")) return { - "serviceofferingid": self._get_service_offering( - name=lc["service_offering"] + "displayname": name, + "serviceofferingid": self.get_service_offering( + name=self.launch["service_offering"] ).get("id"), - "affinitygroupnames": lc.get("affinity_groups"), - "securitygroupnames": lc.get("security_groups"), - "templateid": self._get_template(name=lc["template"]).get("id"), - "zoneid": self._get_zone(name=lc["zone"]).get("id"), + "affinitygroupnames": self.launch.get("affinity_groups"), + "securitygroupnames": self.launch.get("security_groups"), + "templateid": self.get_template(name=self.launch["template"]).get("id"), + "zoneid": self.get_zone(name=self.launch["zone"]).get("id"), "userdata": user_data, - "keypair": lc.get("ssh_key"), - "group": lc.get("group"), - "rootdisksize": lc.get("root_disk_size"), + "keypair": self.launch.get("ssh_key"), + "group": self.launch.get("group"), + "rootdisksize": self.launch.get("root_disk_size"), } - def scale_up(self, diff: int): - log.info(f"scaling up {diff}") - - if diff > 0: - lc = self.launch_config.copy() - params = self._get_deploy_params(lc) - - while diff > 0: - - name = self.get_unique_name() - params.update( - { - "name": name, - } - ) - lc_tags = lc.get("tags", {}) - tags = [ + def get_current_instances(self) -> List[GenericCloudInstance]: + filter_tag = f"scalr={self.filter}" + log.info(f"cloudstack: Querying with filter_tag: {filter_tag}") + servers = self.cs.listVirtualMachines( + tags=[ { "key": "scalr", - "value": self.name, + "value": self.filter, } - ] - for key, value in lc_tags.items(): - if key != self.name: - tags.append({"key": key, "value": value}) + ], + fetch_list=True, + ) + return [ + GenericCloudInstance( + id=server["id"], + name=server["name"], + status=server["state"].lower(), + ) + for server in sorted(servers, key=lambda i: i["created"]) + ] - if not self.dry_run: - server = self.cs.deployVirtualMachine(**params) - self.cs.createTags( - resourceids=[ - server["id"], - ], - resourcetype="UserVm", - tags=tags, - ) - log.info(f"Creating server name={name}") - else: - log.info(f"Dry run creating server name={name}") - diff -= 1 + def ensure_instances_running(self): + for server in self.get_current_instances(): + log.info(f"cloudstack: Server {server.name} status {server.status}") + if server.status in ["stopping", "stopped"]: + self.cs.startVirtualMachine(id=server.id) + log.info(f"cloudstack: Server {server.name} started") + + def deploy_instance(self, name: str): + params = self.get_params(name=name) + + tags = self.launch.get("tags", {}) + tags = [ + { + "key": "scalr", + "value": self.filter, + } + ] + server = self.cs.deployVirtualMachine(**params) + self.cs.createTags( + resourceids=[ + server["id"], + ], + resourcetype="UserVm", + tags=tags, + ) - def scale_down(self, diff: int): - log.info(f"scaling down {diff}") - while diff > 0: - server = self.get_selected_server() - if not self.dry_run: - self.cs.destroyVirtualMachine(id=server["id"]) - log.info(f"Deleting server id={server['id']}") - else: - log.info(f"Dry run deleting server id={server['id']}") - diff -= 1 + def destroy_instance(self, instance: GenericCloudInstance): + log.info(f"cloudstack: Destroying instance {instance}") + self.cs.destroyVirtualMachine(id=instance.id) diff --git a/scalr/cloud/adapters/exoscale.py b/scalr/cloud/adapters/exoscale.py index 897e669..4522fe0 100644 --- a/scalr/cloud/adapters/exoscale.py +++ b/scalr/cloud/adapters/exoscale.py @@ -1,6 +1,7 @@ import os + from cs import CloudStack -from scalr.cloud.cloudstack import CloudstackCloudAdapter +from scalr.cloud.adapters.cloudstack import CloudstackCloudAdapter from scalr.log import log @@ -13,11 +14,11 @@ def __init__(self): secret=os.getenv("EXOSCALE_API_SECRET"), ) - def _get_deploy_params(self, lc): - params = super()._get_deploy_params(lc) + def get_params(self, name) -> dict: + params = super().get_params(name=name) params.update( { - "ipv6": lc.get("use_ipv6"), + "ipv6": self.launch.get("use_ipv6"), } ) return params diff --git a/scalr/cloud/factory.py b/scalr/cloud/factory.py index a047771..62d6c86 100644 --- a/scalr/cloud/factory.py +++ b/scalr/cloud/factory.py @@ -1,9 +1,9 @@ from scalr.cloud import CloudAdapter from scalr.cloud.adapters.cloudscale_ch import CloudscaleCloudAdapter - -# from scalr.cloud.adapters.cloudstack import CloudstackCloudAdapter +from scalr.cloud.adapters.cloudstack import CloudstackCloudAdapter from scalr.cloud.adapters.digitalocean import DigitaloceanCloudAdapter from scalr.cloud.adapters.dummy import DummyCloudAdapter +from scalr.cloud.adapters.exoscale import ExoscaleCloudAdapter from scalr.cloud.adapters.hcloud import HcloudCloudAdapter from scalr.cloud.adapters.vultr import VultrCloudAdapter from scalr.log import log @@ -14,7 +14,8 @@ class CloudAdapterFactory: ADAPTERS = { "cloudscale_ch": CloudscaleCloudAdapter, - # "cloudstack": CloudstackCloudAdapter, + "cloudstack": CloudstackCloudAdapter, + "exoscale": ExoscaleCloudAdapter, "digitalocean": DigitaloceanCloudAdapter, "hcloud": HcloudCloudAdapter, "vultr": VultrCloudAdapter, diff --git a/scalr/version.py b/scalr/version.py index ae6db5f..ea370a8 100644 --- a/scalr/version.py +++ b/scalr/version.py @@ -1 +1 @@ -__version__ = "0.11.0" +__version__ = "0.12.0" diff --git a/setup.py b/setup.py index cb7a79a..7807b20 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name="scalr-ngine", - version=version['__version__'], + version=version["__version__"], author="René Moser", author_email="mail@renemoser.net", license="MIT", @@ -40,10 +40,10 @@ ], install_requires=install_requires, tests_require=tests_require, - python_requires='>=3.6', + python_requires=">=3.8", entry_points={ - 'console_scripts': [ - 'scalr-ngine = scalr.app:main', + "console_scripts": [ + "scalr-ngine = scalr.app:main", ], }, ) diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..52fc435 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,18 @@ +import pytest +from scalr.cloud.factory import CloudAdapterFactory +from scalr.policy.factory import PolicyAdapterFactory +from scalr.version import __version__ + + +def test_version(): + assert __version__ == "0.12.0" + + +def test_not_implementend_cloud(): + with pytest.raises(NotImplementedError, match=r".*does-not-exist.*"): + cloud = CloudAdapterFactory.create(name="does-not-exist") + + +def test_not_implementend_policy(): + with pytest.raises(NotImplementedError, match=r".*does-not-exist.*"): + policy = PolicyAdapterFactory.create(source="does-not-exist") diff --git a/tox.ini b/tox.ini index bef8a53..68ddcd2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] -envlist = py{36,37,38} +envlist = py{38,310} skip_missing_interpreters = True skipsdist = True [gh-actions] python = - 3.6: py36 - 3.7: py37 3.8: py38 + 3.10: py310 [testenv] changedir = tests