Merge lp:~dpb/juju-deployer/api-endpoints into lp:juju-deployer

Proposed by David Britton
Status: Merged
Merged at revision: 120
Proposed branch: lp:~dpb/juju-deployer/api-endpoints
Merge into: lp:juju-deployer
Diff against target: 230 lines (+145/-4)
4 files modified
deployer/env/go.py (+24/-4)
deployer/tests/test_goenv.py (+85/-0)
deployer/tests/test_utils.py (+22/-0)
deployer/utils.py (+14/-0)
To merge this branch: bzr merge lp:~dpb/juju-deployer/api-endpoints
Reviewer Review Type Date Requested Status
juju-deployers Pending
Review via email: mp+230190@code.launchpad.net

Commit message

Consult juju api-endpoints if using gojuju

Description of the change

Consult juju api-endpoints if using gojuju. Fall back to using the old mechanism of looking at juju status for node 0.

I created a daily build PPA for this as well, so we can make forward progress on testing:

https://code.launchpad.net/~davidpbritton/+archive/ubuntu/juju-deployer-devonly

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Thanks for the patch.. I have some mixed feelings about using api-endpoints. All api-endpoints is giving us is the addresses. The information we already want is in the the jenv file (credentials, state-servers, and critically the ca-cert to validate the server). There's a class method on the jujuclient Environment to enable connecting using the jenv file given an environment name .connect("foobar"). I'd like to push the logic for trying servers there, and just have deployer call out to it as ideally this should be abstracted for folks at the client library level.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

actually i'm curious if there are places where the first state server from api-endpoints or that's listed in the .jenv file under state-servers isn't the appropriate one.

Revision history for this message
David Britton (dpb) wrote :

On Sat, Aug 23, 2014 at 04:57:33PM -0000, Kapil Thangavelu wrote:
> actually i'm curious if there are places where the first state server
> from api-endpoints or that's listed in the .jenv file under
> state-servers isn't the appropriate one.

Good question... IME, Yes there are:

- In maas there can often be multiple network interfaces, each of which
  gets an IP address... The name is supposed to point to the primary
  IP, but it depends on maas managing the DNS, and also my client having
  access to do queries on MAAS (depending on the network config, this
  can be a problem). In these cases, I get 3 or 4 lines of addresses to
  check. :)

- In openstack, often the primary ip is an internal address and the
  floating IP is an external address. Juju exposes both as
  api-endpoints, often (maybe always), the first is the primary.

> --
> https://code.launchpad.net/~davidpbritton/juju-deployer/api-endpoints/+merge/230190
> You are the owner of lp:~davidpbritton/juju-deployer/api-endpoints.

--
David Britton <email address hidden>

Revision history for this message
David Britton (dpb) wrote :

On Sat, Aug 23, 2014 at 04:50:07PM -0000, Kapil Thangavelu wrote:
> there, and just have deployer call out to it as ideally this should be
> abstracted for folks at the client library level.

Makes sense... let me eval what is necessary there. I'll set to WIP
until then.

--
David Britton <email address hidden>

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

i went ahead and reimplemented this against the jujuclient over the weekend
fwiw, i'm keen on the other patch for placement. let's chat on irc.

On Mon, Aug 25, 2014 at 4:19 PM, David Britton <email address hidden>
wrote:

> The proposal to merge lp:~davidpbritton/juju-deployer/api-endpoints into
> lp:juju-deployer has been updated.
>
> Status: Needs review => Work in progress
>
> For more details, see:
>
> https://code.launchpad.net/~davidpbritton/juju-deployer/api-endpoints/+merge/230190
> --
>
> https://code.launchpad.net/~davidpbritton/juju-deployer/api-endpoints/+merge/230190
> Your team juju-deployers is requested to review the proposed merge of
> lp:~davidpbritton/juju-deployer/api-endpoints into lp:juju-deployer.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'deployer/env/go.py'
2--- deployer/env/go.py 2014-03-05 23:18:36 +0000
3+++ deployer/env/go.py 2014-08-08 21:50:04 +0000
4@@ -4,7 +4,7 @@
5 import time
6
7 from .base import BaseEnvironment
8-from ..utils import ErrorExit, _get_juju_home, yaml_load
9+from ..utils import ErrorExit, _get_juju_home, yaml_load, is_port_up
10
11 from jujuclient import (
12 EnvError,
13@@ -64,9 +64,14 @@
14
15 def connect(self):
16 if not self.api_endpoint:
17- # Should really have a cheaper/faster way of getting the endpoint
18- # ala juju status 0 for a get_endpoint method.
19- self.get_cli_status()
20+ endpoint = self._get_cli_endpoint()
21+ if endpoint is None:
22+ # Fallback to older methods
23+ self.log.info(
24+ "Could not get endpoints directly, will infer from status")
25+ status = self.get_cli_status()
26+ endpoint = (status["machines"]["0"]["dns-name"], 17070)
27+ self.api_endpoint = "wss://%s:%s/" % endpoint
28 while True:
29 try:
30 self.client = EnvironmentClient(self.api_endpoint)
31@@ -92,6 +97,21 @@
32 return {}
33 raise
34
35+ def _get_cli_endpoint(self):
36+ """ Consult juju for api-endpoints, returns None if not found """
37+ params = self._named_env(["juju", "api-endpoints"])
38+ try:
39+ result = self._check_call(
40+ params, self.log, "Error getting endpoints!")
41+ except ErrorExit:
42+ return None
43+ endpoints = result.rstrip().split()
44+ for endpoint in endpoints:
45+ (host, port) = endpoint.split(":")
46+ if is_port_up(host, port):
47+ return (host, port)
48+ return None
49+
50 def get_cli_status(self):
51 status = super(GoEnvironment, self).get_cli_status()
52 # Opportunistic, see connect method comment.
53
54=== modified file 'deployer/tests/test_goenv.py'
55--- deployer/tests/test_goenv.py 2014-02-22 23:11:02 +0000
56+++ deployer/tests/test_goenv.py 2014-08-08 21:50:04 +0000
57@@ -1,14 +1,99 @@
58 import logging
59+from mock import (
60+ MagicMock,
61+ patch,
62+)
63 import os
64 import time
65 import sys
66 import unittest
67
68 from deployer.env.go import GoEnvironment
69+from ..utils import ErrorExit
70
71 from .base import Base
72
73
74+class GoEnvTests(Base):
75+ def setUp(self):
76+ self.env = GoEnvironment("unit-tests-go")
77+
78+ @patch("deployer.env.go.is_port_up")
79+ @patch("deployer.env.go.GoEnvironment._check_call")
80+ def test__get_cli_endpoint_single_endpoint(self, call, isup):
81+ """Single api-endpoint."""
82+ isup.return_value = True
83+ call.return_value = "one.two:12\n"
84+ endpoint = self.env._get_cli_endpoint()
85+ self.assertEqual(endpoint, ("one.two", "12"))
86+
87+ @patch("deployer.env.go.is_port_up")
88+ @patch("deployer.env.go.GoEnvironment._check_call")
89+ def test__get_cli_endpoint_multiple_endpoints(self, call, isup):
90+ """
91+ Multiple api-endpoints. Since list is traversed in order
92+ make sure we reutrn something interesting, like the last one
93+ symbolizing the first two not being active api endpoints
94+ """
95+ def _get_endpoint(host, value):
96+ if host == "one.four":
97+ return True
98+ else:
99+ return False
100+ isup.side_effect = _get_endpoint
101+ call.return_value = "one.two:12\none.three:13\none.four:14\n"
102+ endpoint = self.env._get_cli_endpoint()
103+ self.assertEqual(endpoint, ("one.four", "14"))
104+
105+ @patch("deployer.env.go.is_port_up")
106+ @patch("deployer.env.go.GoEnvironment._check_call")
107+ def test__get_cli_endpoint_multiple_endpoints_exhausted(self, call, isup):
108+ """
109+ Multiple api-endpoints. Check when list is exhausted that we
110+ return None appropriately.
111+ """
112+ isup.return_value = False
113+ call.return_value = "one.two:12\none.three:13\none.four:14\n"
114+ self.assertIsNone(self.env._get_cli_endpoint())
115+
116+ @patch("deployer.env.go.GoEnvironment._check_call")
117+ def test__get_cli_endpoint_empty(self, call):
118+ """Empty api-endpoints list."""
119+ call.return_value = "\n"
120+ self.assertIsNone(self.env._get_cli_endpoint())
121+
122+ @patch("deployer.env.go.GoEnvironment._check_call")
123+ def test__get_cli_endpoint_error(self, call):
124+ """Juju api-endpoints error should return None."""
125+ call.side_effect = ErrorExit
126+ self.assertIsNone(self.env._get_cli_endpoint())
127+
128+ @patch("deployer.env.go.GoEnvironment.log")
129+ @patch("deployer.env.go.GoEnvironment._get_token")
130+ @patch("deployer.env.go.EnvironmentClient")
131+ @patch("deployer.env.go.GoEnvironment._get_cli_endpoint")
132+ def test_connect_no_api_endpoint(self, getendpoint, *args):
133+ """Test connect happy path."""
134+ getendpoint.return_value = ("one.two", "12")
135+ self.env.connect()
136+ self.assertEqual(self.env.api_endpoint, "wss://one.two:12/")
137+
138+ @patch("deployer.env.go.GoEnvironment._get_token")
139+ @patch("deployer.env.go.EnvironmentClient")
140+ @patch("deployer.env.go.GoEnvironment.log")
141+ @patch("deployer.env.go.GoEnvironment.get_cli_status")
142+ @patch("deployer.env.go.GoEnvironment._get_cli_endpoint")
143+ def test_connect_no_api_endpoint_command(
144+ self, get_endpoint, get_clistat, log, *args):
145+ """Test connect happy path on older juju w/o api-endpoints."""
146+ get_endpoint.return_value = None
147+ get_clistat.return_value = {"machines": {"0": {"dns-name": "foo"}}}
148+ self.env.connect()
149+ log.info.assert_called_with(
150+ "Could not get endpoints directly, will infer from status")
151+ self.assertEqual(self.env.api_endpoint, "wss://foo:17070/")
152+
153+
154 # Takes roughly about 6m on core2 + ssd, mostly cloudinit time
155 @unittest.skipIf(
156 (not bool(os.environ.get("TEST_ENDPOINT"))),
157
158=== modified file 'deployer/tests/test_utils.py'
159--- deployer/tests/test_utils.py 2014-02-22 23:11:02 +0000
160+++ deployer/tests/test_utils.py 2014-08-08 21:50:04 +0000
161@@ -1,4 +1,6 @@
162+import errno
163 import os
164+import socket
165 from StringIO import StringIO
166 from subprocess import CalledProcessError
167
168@@ -18,6 +20,7 @@
169 HTTPError,
170 mkdir,
171 URLError,
172+ is_port_up,
173 )
174
175
176@@ -68,6 +71,25 @@
177 # 1 failure + 3 retries
178 self.assertEquals(len(check_output.call_args_list), 3)
179
180+ @patch('socket.create_connection')
181+ def test_is_port_up_valid(self, create_connection):
182+ """Happy path through the is_port_up test"""
183+ self.assertTrue(is_port_up("foo.example.com", "123"))
184+
185+ @patch('socket.create_connection')
186+ def test_is_port_up_unknown_error(self, create_connection):
187+ """Unknown error should re-raise"""
188+ create_connection.side_effect = Exception
189+ self.assertRaises(Exception, is_port_up, ("foo.example.com", "123"))
190+
191+ @patch('socket.create_connection')
192+ def test_is_port_up_known_error(self, create_connection ):
193+ """Known error should return false"""
194+ e = socket.error()
195+ e.errno = errno.ETIMEDOUT
196+ create_connection.side_effect = e
197+ self.assertFalse(is_port_up("foo.example.com", "123"))
198+
199
200 class TestMkdir(Base):
201
202
203=== modified file 'deployer/utils.py'
204--- deployer/utils.py 2014-02-22 23:17:02 +0000
205+++ deployer/utils.py 2014-08-08 21:50:04 +0000
206@@ -16,6 +16,7 @@
207 exists as path_exists,
208 )
209
210+import socket
211 import stat
212 import subprocess
213 import time
214@@ -372,3 +373,16 @@
215 if not 'default' in conf:
216 raise ValueError("No Environment specified")
217 return conf['default']
218+
219+
220+def is_port_up(address, port):
221+ """ Given address/port, return true/false if it's up """
222+ try:
223+ socket.create_connection((address, port))
224+ return True
225+ except socket.error as err:
226+ if err.errno in (
227+ errno.ETIMEDOUT, errno.ECONNREFUSED, errno.ECONNRESET):
228+ return False
229+ else:
230+ raise

Subscribers

People subscribed via source and target branches