-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial openstack support #113
Conversation
22c7a50
to
44115e2
Compare
44115e2
to
cc7e1e4
Compare
I recently noticed that on the CLI, I don't need to provide the network information like I do through the API. I'm curious how the CLI knows which network to create the instance on and if we could use that same knowledge to acquire the network to use when using the API. |
I'm also going on the assumption that we want a floating IP associated with instances that we launch. Obviously not every openstack deployment in the wild will require that, but I don't think that's something we need to worry about yet. |
146824b
to
9e9d9bd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this work, James! By and large, this looks good. I have some thoughts inline.
pycloudlib/openstack/cloud.py
Outdated
instance = self.conn.compute.create_server( | ||
name=self.tag, | ||
image_id=image_id, | ||
flavor_id=self.conn.compute.find_flavor(instance_type).id, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flavors are not necessarily the same across OpenStack deployments: will this produce a meaningful error message if used against a cloud without "m1.small" defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't realize that about flavors. I'll update it accordingly.
pycloudlib/openstack/instance.py
Outdated
self.floating_ip = self.conn.create_floating_ip( | ||
wait=True, | ||
) | ||
tries = 30 | ||
while tries: | ||
try: | ||
self.conn.compute.add_floating_ip_to_server( | ||
self.server, | ||
self.floating_ip.floating_ip_address | ||
) | ||
break | ||
except BadRequestException as e: | ||
if 'Instance network is not ready yet' in str(e): | ||
tries -= 1 | ||
time.sleep(1) | ||
continue | ||
raise e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will assign a new floating IP address to existing instances which are found via (e.g.) get_instance
, which I don't think is what we want to do.
(I guess this should move up into launch
?)
pycloudlib/openstack/instance.py
Outdated
self.conn.compute.delete_server(self.server.id) | ||
self.conn.delete_floating_ip(self.floating_ip.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we ensure we delete the floating IP even if the server delete fails?
pycloudlib/openstack/instance.py
Outdated
except ConflictException as e: | ||
if 'while it is in vm_state active' in str(e): | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An explanatory comment here would be good!
@@ -27,6 +27,7 @@ def read_readme(): | |||
"azure-mgmt-compute >= 13.0.0, < 17", | |||
"azure-cli-core >= 2.9.1", | |||
"knack >= 0.7.1", | |||
"python-openstackclient >= 5.2.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This version declares a minimum version of Python 3.6. We might be OK, because (last I saw) OpenStack manages its Python dependencies globally, so that could be an artifically high lower bound in this specific case.
@mock.patch('pycloudlib.key.KeyPair.public_key_content', | ||
new_callable=mock.PropertyMock, | ||
return_value='pretend public key') | ||
@mock.patch('openstack.connect', spec=Connection) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this spec
is doing what you want here:
In [1]: import unittest.mock as mock
In [3]: from openstack.connection import Connection
In [4]: p = mock.patch("openstack.connect", spec=Connection)
In [5]: m = p.start()
In [6]: m() # this is what `openstack.connect()` will return
Out[6]: <MagicMock name='connect()' id='140181990886512'>
In [7]: m # and I think you actually want it to return this
Out[7]: <openstack.connection.Connection at 0x7f7ea9c62fa0>
class TestOpenstackKeypair: | ||
"""Tests covering _get_openstack_keypair.""" | ||
|
||
def test_keypair_doesnt_exist(self, m_openstack, m_keypair): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_keypair_doesnt_exist(self, m_openstack, m_keypair): | |
def test_keypair_doesnt_exist(self, m_openstack, _m_keypair): |
cloud._get_openstack_keypair() | ||
assert 1 == cloud.conn.create_keypair.call_count | ||
|
||
def test_keypairs_match(self, m_openstack, m_keypair): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_keypair
threw me off here: I assumed that would be a mock of pycloudlib.key.KeyPair
(making this test very confusing :p). Perhaps m_public_key_content
? Still a little confusing, given it's a property mock (whereas you'd never call public_key_content
of course).
Addressed the code comments. Still need to address the test comments. @OddBloke , I addressed the floating IP issue a little differently than we initially discussed, so you might want to take a look. |
Updated the test contents, but now I have new floating ip tests to write 😉 |
Added new tests. Should be ready for re-review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Floating IP stuff LGTM, I've left a few inline comments, all of which are nits. I'm going to run some tests locally before approving but, assuming that's all good, this will be good to land.
pycloudlib/openstack/instance.py
Outdated
self.delete_floating_ip = False | ||
self.floating_ip = self._get_existing_floating_ip() | ||
if self.floating_ip is None: | ||
self._create_and_attach_floating_id() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit, but self.floating_ip = self._create_and_attach_floating_id()
feels a little easier to follow; it would give _get_...
and _create_...
a (more) similar API and means that self.floating_ip
is only set in one place.
NETWORK_IPS = [ | ||
{'floating_ip_address': '10.0.0.4'}, | ||
{'floating_ip_address': '10.0.0.3'}, | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another nit, but if the code under test incorrectly did the equivalent of chain(*[d.values() for d in NETWORK_IPS])
, we wouldn't catch that with this input.
def test_existing_floating_ip(self, m_create): | ||
"""Test that if a server has an existing floating IP, we use it.""" | ||
m_connection = mock.Mock() | ||
m_server = m_connection.compute.get_server.return_value = mock.Mock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit; I don't think we need this final assignment:
m_server = m_connection.compute.get_server.return_value = mock.Mock() | |
m_server = m_connection.compute.get_server.return_value |
In [6]: from unittest import mock
In [7]: m_connection = mock.Mock()
In [8]: m_server = m_connection.compute.get_server.return_value
In [9]: m_server
Out[9]: <Mock name='mock.compute.get_server()' id='140123963984144'>
In [10]: m_server = m_connection.compute.get_server.return_value = mock.Mock()
In [11]: m_server
Out[11]: <Mock name='mock.compute.get_server()' id='140123963995232'>
Co-authored-by: Daniel Watkins <oddbloke@ubuntu.com>
Co-authored-by: Daniel Watkins <oddbloke@ubuntu.com>
0f1b4cf
to
02ad4b5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the work!
At this point I think the basic functionality is usable and reviewable.
Everything in the base_api.py test works, so the basics all work. I intentionally didn't add an example since all clouds should share the same base API right now. We should really only need one example unless we want to showcase specialized use cases of a specific cloud.