pycloudlib 1!2.0.0__tar.gz → 1!2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {pycloudlib-1!2.0.0/pycloudlib.egg-info → pycloudlib-1!2.1.0}/PKG-INFO +1 -1
  2. pycloudlib-1!2.1.0/VERSION +1 -0
  3. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/cloud.py +2 -6
  4. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/instance.py +6 -1
  5. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/key.py +5 -0
  6. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/cloud.py +3 -1
  7. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/oci/instance.py +19 -1
  8. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0/pycloudlib.egg-info}/PKG-INFO +1 -1
  9. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib.egg-info/SOURCES.txt +8 -5
  10. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib.egg-info/top_level.txt +1 -0
  11. pycloudlib-1!2.1.0/tests/__init__.py +0 -0
  12. pycloudlib-1!2.1.0/tests/integration_tests/__init__.py +0 -0
  13. pycloudlib-1!2.1.0/tests/integration_tests/test_public_api.py +146 -0
  14. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/tox.ini +25 -0
  15. pycloudlib-1!2.0.0/VERSION +0 -1
  16. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/LICENSE +0 -0
  17. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/MANIFEST.in +0 -0
  18. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/README.md +0 -0
  19. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/__init__.py +0 -0
  20. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/azure/__init__.py +0 -0
  21. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/azure/cloud.py +0 -0
  22. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/azure/instance.py +0 -0
  23. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/azure/tests/__init__.py +0 -0
  24. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/azure/tests/test_cloud.py +0 -0
  25. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/azure/util.py +0 -0
  26. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/config.py +0 -0
  27. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/constants.py +0 -0
  28. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ec2/__init__.py +0 -0
  29. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ec2/cloud.py +0 -0
  30. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ec2/instance.py +0 -0
  31. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ec2/util.py +0 -0
  32. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ec2/vpc.py +0 -0
  33. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/errors.py +0 -0
  34. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/__init__.py +0 -0
  35. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/cloud.py +0 -0
  36. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/errors.py +0 -0
  37. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/instance.py +0 -0
  38. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/tests/__init__.py +0 -0
  39. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/tests/test_cloud.py +0 -0
  40. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/gce/util.py +0 -0
  41. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/__init__.py +0 -0
  42. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/_util.py +0 -0
  43. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/cloud.py +0 -0
  44. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/errors.py +0 -0
  45. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/instance.py +0 -0
  46. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/tests/__init__.py +0 -0
  47. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/ibm/tests/test_util.py +0 -0
  48. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/__init__.py +0 -0
  49. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/_images.py +0 -0
  50. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/defaults.py +0 -0
  51. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/instance.py +0 -0
  52. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/tests/__init__.py +0 -0
  53. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/tests/test_cloud.py +0 -0
  54. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/tests/test_defaults.py +0 -0
  55. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/tests/test_images.py +0 -0
  56. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/lxd/tests/test_instance.py +0 -0
  57. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/oci/__init__.py +0 -0
  58. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/oci/cloud.py +0 -0
  59. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/oci/utils.py +0 -0
  60. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/openstack/__init__.py +0 -0
  61. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/openstack/cloud.py +0 -0
  62. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/openstack/errors.py +0 -0
  63. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/openstack/instance.py +0 -0
  64. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/result.py +0 -0
  65. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib/util.py +0 -0
  66. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib.egg-info/dependency_links.txt +0 -0
  67. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib.egg-info/requires.txt +0 -0
  68. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pycloudlib.egg-info/zip-safe +0 -0
  69. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/pyproject.toml +0 -0
  70. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/requirements.txt +0 -0
  71. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/setup.cfg +0 -0
  72. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/setup.py +0 -0
  73. {pycloudlib-1!2.0.0 → pycloudlib-1!2.1.0}/test-requirements.txt +0 -0
  74. {pycloudlib-1!2.0.0/pycloudlib/tests → pycloudlib-1!2.1.0/tests/unit_tests}/__init__.py +0 -0
  75. {pycloudlib-1!2.0.0/pycloudlib/tests → pycloudlib-1!2.1.0/tests/unit_tests}/test_cloud.py +0 -0
  76. {pycloudlib-1!2.0.0/pycloudlib/tests → pycloudlib-1!2.1.0/tests/unit_tests}/test_config.py +0 -0
  77. {pycloudlib-1!2.0.0/pycloudlib/tests → pycloudlib-1!2.1.0/tests/unit_tests}/test_errors.py +0 -0
  78. {pycloudlib-1!2.0.0/pycloudlib/tests → pycloudlib-1!2.1.0/tests/unit_tests}/test_instance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycloudlib
3
- Version: 1!2.0.0
3
+ Version: 1!2.1.0
4
4
  Summary: Python library to launch, interact, and snapshot cloud instances
5
5
  Home-page: https://launchpad.net/pycloudlib
6
6
  Author: pycloudlib-devs
@@ -0,0 +1 @@
1
+ 1!2.1.0
@@ -55,14 +55,10 @@ class BaseCloud(ABC):
55
55
  user = getpass.getuser()
56
56
  self.key_pair = KeyPair(
57
57
  public_key_path=os.path.expandvars(
58
- os.path.expanduser(
59
- self.config.get(
60
- "public_key_path", f"~{user}/.ssh/id_rsa.pub"
61
- )
62
- )
58
+ self.config.get("public_key_path", f"~{user}/.ssh/id_rsa.pub")
63
59
  ),
64
60
  private_key_path=os.path.expandvars(
65
- os.path.expanduser(self.config.get("private_key_path", ""))
61
+ self.config.get("private_key_path", "")
66
62
  ),
67
63
  name=self.config.get("key_name", user),
68
64
  )
@@ -199,7 +199,12 @@ class BaseInstance(ABC):
199
199
 
200
200
  This will clean out specifically the cloud-init files and system logs.
201
201
  """
202
- self.execute("sudo cloud-init clean --logs")
202
+ result = self.execute("sudo cloud-init clean --logs --machine-id")
203
+ if result.failed:
204
+ # --machine-id likely isn't supported on this version.
205
+ # Manually reset machine-id even though this is less portable
206
+ self.execute("sudo cloud-init clean --logs")
207
+ self.execute("sudo echo 'uninitialized' > /etc/machine-id")
203
208
  self.execute("sudo rm -rf /var/log/syslog")
204
209
 
205
210
  def _run_command(self, command, stdin):
@@ -1,6 +1,8 @@
1
1
  # This file is part of pycloudlib. See LICENSE file for license information.
2
2
  """Base Key Class."""
3
3
 
4
+ import os
5
+
4
6
 
5
7
  class KeyPair:
6
8
  """Key Class."""
@@ -24,6 +26,9 @@ class KeyPair:
24
26
  else:
25
27
  self.private_key_path = self.public_key_path.replace(".pub", "")
26
28
 
29
+ self.public_key_path = os.path.expanduser(self.public_key_path)
30
+ self.private_key_path = os.path.expanduser(self.private_key_path)
31
+
27
32
  def __str__(self):
28
33
  """Create string representation of class."""
29
34
  return "KeyPair({}, {}, name={})".format(
@@ -2,6 +2,7 @@
2
2
  """LXD Cloud type."""
3
3
  import warnings
4
4
  from abc import ABC
5
+ from itertools import count
5
6
 
6
7
  import yaml
7
8
 
@@ -20,6 +21,7 @@ class _BaseLXD(BaseCloud, ABC):
20
21
  _daily_remote = "ubuntu-daily"
21
22
  _releases_remote = "ubuntu"
22
23
  _lxd_instance_cls = LXDInstance
24
+ _instance_counter = count()
23
25
  _is_container: bool
24
26
 
25
27
  def clone(self, base, new_instance_name):
@@ -291,7 +293,7 @@ class _BaseLXD(BaseCloud, ABC):
291
293
  f" Found: {image_id}"
292
294
  )
293
295
  instance = self.init(
294
- name=name,
296
+ name=name or f"{self.tag}-{next(self._instance_counter)}",
295
297
  image_id=image_id,
296
298
  ephemeral=ephemeral,
297
299
  network=network,
@@ -2,6 +2,8 @@
2
2
  # This file is part of pycloudlib. See LICENSE file for license information.
3
3
  """OCI instance."""
4
4
 
5
+ from time import sleep
6
+
5
7
  import oci
6
8
 
7
9
  from pycloudlib.errors import PycloudlibError
@@ -96,7 +98,23 @@ class OciInstance(BaseInstance):
96
98
 
97
99
  def _do_restart(self, **kwargs):
98
100
  """Restart the instance."""
99
- self.compute_client.instance_action(self.instance_data.id, "RESET")
101
+ last_exception = None
102
+ for _ in range(30):
103
+ try:
104
+ self.compute_client.instance_action(
105
+ self.instance_data.id, "RESET"
106
+ )
107
+ return
108
+ except oci.exceptions.ServiceError as e:
109
+ last_exception = e
110
+ if last_exception.status != 409:
111
+ raise
112
+ self._log.debug(
113
+ "Received 409 attempting to RESET instance. Retrying"
114
+ )
115
+ sleep(0.5)
116
+ if last_exception:
117
+ raise last_exception
100
118
 
101
119
  def shutdown(self, wait=True, **kwargs):
102
120
  """Shutdown the instance.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pycloudlib
3
- Version: 1!2.0.0
3
+ Version: 1!2.1.0
4
4
  Summary: Python library to launch, interact, and snapshot cloud instances
5
5
  Home-page: https://launchpad.net/pycloudlib
6
6
  Author: pycloudlib-devs
@@ -65,8 +65,11 @@ pycloudlib/openstack/__init__.py
65
65
  pycloudlib/openstack/cloud.py
66
66
  pycloudlib/openstack/errors.py
67
67
  pycloudlib/openstack/instance.py
68
- pycloudlib/tests/__init__.py
69
- pycloudlib/tests/test_cloud.py
70
- pycloudlib/tests/test_config.py
71
- pycloudlib/tests/test_errors.py
72
- pycloudlib/tests/test_instance.py
68
+ tests/__init__.py
69
+ tests/integration_tests/__init__.py
70
+ tests/integration_tests/test_public_api.py
71
+ tests/unit_tests/__init__.py
72
+ tests/unit_tests/test_cloud.py
73
+ tests/unit_tests/test_config.py
74
+ tests/unit_tests/test_errors.py
75
+ tests/unit_tests/test_instance.py
File without changes
File without changes
@@ -0,0 +1,146 @@
1
+ import ipaddress
2
+ from contextlib import contextmanager, suppress
3
+ from pathlib import Path
4
+ from tempfile import TemporaryDirectory
5
+ from typing import Generator
6
+
7
+ import pytest
8
+
9
+ import pycloudlib
10
+ from pycloudlib.cloud import BaseCloud
11
+ from pycloudlib.instance import BaseInstance
12
+ from pycloudlib.util import LTS_RELEASES
13
+
14
+ cloud_config = """\
15
+ #cloud-config
16
+ runcmd:
17
+ - echo 'hello' >> /var/tmp/example.txt
18
+ """
19
+
20
+
21
+ @pytest.fixture
22
+ def cloud(request):
23
+ cloud_instance: BaseCloud = request.param(
24
+ tag="pycl-test",
25
+ timestamp_suffix=True,
26
+ )
27
+
28
+ if isinstance(cloud_instance, pycloudlib.EC2):
29
+ cloud_instance.upload_key(
30
+ public_key_path=cloud_instance.config["public_key_path"],
31
+ private_key_path=cloud_instance.config["private_key_path"],
32
+ name=cloud_instance.tag,
33
+ )
34
+
35
+ yield cloud_instance
36
+
37
+ if isinstance(cloud_instance, pycloudlib.EC2):
38
+ cloud_instance.delete_key(name=cloud_instance.tag)
39
+
40
+
41
+ @contextmanager
42
+ def launch_instance(
43
+ cloud_instance: BaseCloud, **kwargs
44
+ ) -> Generator[BaseInstance, None, None]:
45
+ instance = cloud_instance.launch(**kwargs)
46
+ try:
47
+ yield instance
48
+ finally:
49
+ instance.delete()
50
+
51
+
52
+ def assert_example_output(instance: BaseInstance):
53
+ example_output = instance.execute("cat /var/tmp/example.txt").stdout
54
+ assert example_output == "hello"
55
+
56
+
57
+ def exercise_push_pull(instance: BaseInstance):
58
+ with TemporaryDirectory() as tmpdir:
59
+ push_path = Path(tmpdir).joinpath("pushed")
60
+ push_path.write_text("pushed", encoding="utf-8")
61
+ instance.push_file(str(push_path), "/var/tmp/pushed")
62
+ assert (
63
+ "pushed" == instance.execute("cat /var/tmp/pushed").stdout.strip()
64
+ )
65
+
66
+ instance.execute("echo 'pulled' > /var/tmp/pulled")
67
+ pull_path = Path(tmpdir).joinpath("pulled")
68
+ instance.pull_file("/var/tmp/pulled", str(pull_path))
69
+ assert pull_path.read_text(encoding="utf-8").strip() == "pulled"
70
+
71
+
72
+ def exercise_instance(instance: BaseInstance):
73
+ assert instance.name is not None
74
+ assert ipaddress.ip_address(instance.ip)
75
+
76
+ with suppress(NotImplementedError):
77
+ ip_address = instance.add_network_interface()
78
+ try:
79
+ assert ipaddress.ip_address(ip_address)
80
+ finally:
81
+ instance.remove_network_interface(ip_address)
82
+
83
+ exercise_push_pull(instance)
84
+
85
+ assert_example_output(instance)
86
+ boot_id = instance.get_boot_id()
87
+ instance.restart()
88
+ assert boot_id != instance.get_boot_id()
89
+
90
+ assert_example_output(instance)
91
+ instance.shutdown()
92
+
93
+ instance.start()
94
+ assert_example_output(instance)
95
+
96
+
97
+ @pytest.mark.parametrize(
98
+ "cloud",
99
+ [
100
+ pytest.param(
101
+ pycloudlib.Azure, id="azure", marks=pytest.mark.main_check
102
+ ),
103
+ pytest.param(pycloudlib.EC2, id="ec2", marks=pytest.mark.main_check),
104
+ pytest.param(pycloudlib.GCE, id="gce", marks=pytest.mark.main_check),
105
+ pytest.param(pycloudlib.IBM, id="ibm"),
106
+ pytest.param(
107
+ pycloudlib.LXDContainer, id="lxd_container", marks=pytest.mark.ci
108
+ ),
109
+ pytest.param(pycloudlib.LXDVirtualMachine, id="lxd_vm"),
110
+ pytest.param(pycloudlib.OCI, id="oci"),
111
+ # For openstack we first need a reliable way of obtaining the
112
+ # image id
113
+ # pytest.param(pycloudlib.Openstack, id="openstack"),
114
+ ],
115
+ indirect=True,
116
+ )
117
+ def test_public_api(cloud: BaseCloud):
118
+ """Shallow test of (most) public functions in the base API."""
119
+ latest_lts = LTS_RELEASES[-1]
120
+ print(f"Using Ubuntu {latest_lts} release")
121
+ try:
122
+ image_id = cloud.released_image(release=latest_lts)
123
+ except NotImplementedError:
124
+ image_id = cloud.daily_image(release=latest_lts)
125
+ with suppress(NotImplementedError):
126
+ # Not sure there's a great way to test this other than not raising
127
+ cloud.image_serial(image_id)
128
+
129
+ with launch_instance(
130
+ cloud, image_id=image_id, user_data=cloud_config, wait=False
131
+ ) as instance:
132
+ instance.wait()
133
+ exercise_instance(instance)
134
+
135
+ instance.clean()
136
+ result = instance.execute("sudo rm /var/tmp/example.txt")
137
+ snapshot_id = cloud.snapshot(instance)
138
+
139
+ try:
140
+ with launch_instance(
141
+ cloud, image_id=snapshot_id, user_data=cloud_config, wait=False
142
+ ) as instance_from_snapshot:
143
+ instance_from_snapshot.wait()
144
+ exercise_instance(instance_from_snapshot)
145
+ finally:
146
+ cloud.delete_image(snapshot_id)
@@ -84,7 +84,32 @@ envdir = {[tip]envdir}
84
84
  deps = {[tip]deps}
85
85
  commands = {envpython} -m flake8 pycloudlib examples setup.py
86
86
 
87
+ [testenv:integration-tests]
88
+ commands = {envpython} -m pytest --log-cli-level=INFO -svv {posargs:tests/integration_tests}
89
+ deps =
90
+ -rrequirements.txt
91
+ -rtest-requirements.txt
92
+
93
+ [testenv:integration-tests-ci]
94
+ commands = {envpython} -m pytest -m ci --log-cli-level=INFO -svv {posargs:tests/integration_tests}
95
+ deps =
96
+ -rrequirements.txt
97
+ -rtest-requirements.txt
98
+ pytest-xdist
99
+
100
+ [testenv:integration-tests-main-check]
101
+ # Since we can't use GH secrets from a forked PR, run the cloud-based
102
+ # tests after the branch has merged. Better late than never
103
+ commands = {envpython} -m pytest -n 5 -m main_check --log-cli-level=DEBUG -svv {posargs:tests/integration_tests}
104
+ deps = {[testenv:integration-tests-ci]deps}
105
+
87
106
  [flake8]
88
107
  # E203: whitespace before ':' ... This goes against pep8 and black formatting
89
108
  # W503: line break before binary operator
90
109
  ignore = E203, W503
110
+
111
+ [pytest]
112
+ testpaths = tests/unit_tests
113
+ markers =
114
+ ci: run test on as part of continous integration
115
+ main_check: run test after branch has merged to main
@@ -1 +0,0 @@
1
- 1!2.0.0
File without changes
File without changes
File without changes
File without changes
File without changes