Left: | ||
Right: |
OLD | NEW |
---|---|
1 # This file is part of the Juju GUI, which lets users view and manage Juju | 1 # This file is part of the Juju GUI, which lets users view and manage Juju |
2 # environments within a graphical interface (https://launchpad.net/juju-gui). | 2 # environments within a graphical interface (https://launchpad.net/juju-gui). |
3 # Copyright (C) 2013 Canonical Ltd. | 3 # Copyright (C) 2013 Canonical Ltd. |
4 # | 4 # |
5 # This program is free software: you can redistribute it and/or modify it under | 5 # This program is free software: you can redistribute it and/or modify it under |
6 # the terms of the GNU Affero General Public License version 3, as published by | 6 # the terms of the GNU Affero General Public License version 3, as published by |
7 # the Free Software Foundation. | 7 # the Free Software Foundation. |
8 # | 8 # |
9 # This program is distributed in the hope that it will be useful, but WITHOUT | 9 # This program is distributed in the hope that it will be useful, but WITHOUT |
10 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, | 10 # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
62 process. | 62 process. |
63 | 63 |
64 The validation and deployments steps are executed in separate processes. | 64 The validation and deployments steps are executed in separate processes. |
65 It is possible to process only one bundle at the time. | 65 It is possible to process only one bundle at the time. |
66 | 66 |
67 Note that the Deployer is not intended to store request related state: it | 67 Note that the Deployer is not intended to store request related state: it |
68 is instantiated once when the application is bootstrapped and used as a | 68 is instantiated once when the application is bootstrapped and used as a |
69 singleton by all WebSocket requests. | 69 singleton by all WebSocket requests. |
70 """ | 70 """ |
71 | 71 |
72 def __init__(self, apiurl, apiversion, io_loop=None): | 72 def __init__(self, apiurl, apiversion, charmworldurl=None, io_loop=None): |
73 """Initialize the deployer. | 73 """Initialize the deployer. |
74 | 74 |
75 The apiurl argument is the URL of the juju-core WebSocket server. | 75 The apiurl argument is the URL of the juju-core WebSocket server. |
76 The apiversion argument is the Juju API version (e.g. "go"). | 76 The apiversion argument is the Juju API version (e.g. "go"). |
77 """ | 77 """ |
78 self._apiurl = apiurl | 78 self._apiurl = apiurl |
79 self._apiversion = apiversion | 79 self._apiversion = apiversion |
80 if charmworldurl is not None and not charmworldurl.endswith('/'): | |
81 charmworldurl = charmworldurl + '/' | |
82 self._charmworldurl = charmworldurl | |
80 if io_loop is None: | 83 if io_loop is None: |
81 io_loop = IOLoop.current() | 84 io_loop = IOLoop.current() |
82 self._io_loop = io_loop | 85 self._io_loop = io_loop |
83 | 86 |
84 # Deployment validation and importing executors. | 87 # Deployment validation and importing executors. |
85 self._validate_executor = ProcessPoolExecutor(1) | 88 self._validate_executor = ProcessPoolExecutor(1) |
86 self._run_executor = ProcessPoolExecutor(1) | 89 self._run_executor = ProcessPoolExecutor(1) |
87 | 90 |
88 # An observer instance is used to watch the deployments progress. | 91 # An observer instance is used to watch the deployments progress. |
89 self._observer = utils.Observer() | 92 self._observer = utils.Observer() |
90 # Queue stores the deployment identifiers corresponding to the | 93 # Queue stores the deployment identifiers corresponding to the |
91 # currently started/queued jobs. | 94 # currently started/queued jobs. |
92 self._queue = [] | 95 self._queue = [] |
93 # The futures attribute maps deployment identifiers to Futures. | 96 # The futures attribute maps deployment identifiers to Futures. |
94 self._futures = {} | 97 self._futures = {} |
98 # The bundle_ids maps deployment identifiers to bundle_ids. | |
99 self.bundle_ids = {} | |
frankban
2013/11/15 08:48:20
Is bundle_ids really required? ISTM it's not reall
bac
2013/11/15 12:45:10
Good catch. I stopped using it when I began passi
| |
95 | 100 |
96 @gen.coroutine | 101 @gen.coroutine |
97 def validate(self, user, name, bundle): | 102 def validate(self, user, name, bundle): |
98 """Validate the deployment bundle. | 103 """Validate the deployment bundle. |
99 | 104 |
100 The validation is executed in a separate process using the | 105 The validation is executed in a separate process using the |
101 juju-deployer library. | 106 juju-deployer library. |
102 | 107 |
103 Three arguments are provided: | 108 Three arguments are provided: |
104 - user: the current authenticated user; | 109 - user: the current authenticated user; |
105 - name: then name of the bundle to be imported; | 110 - name: then name of the bundle to be imported; |
106 - bundle: a YAML decoded object representing the bundle contents. | 111 - bundle: a YAML decoded object representing the bundle contents. |
107 | 112 |
108 Return a Future whose result is a string representing an error or None | 113 Return a Future whose result is a string representing an error or None |
109 if no error occurred. | 114 if no error occurred. |
110 """ | 115 """ |
111 apiversion = self._apiversion | 116 apiversion = self._apiversion |
112 if apiversion not in SUPPORTED_API_VERSIONS: | 117 if apiversion not in SUPPORTED_API_VERSIONS: |
113 raise gen.Return('unsupported API version: {}'.format(apiversion)) | 118 raise gen.Return('unsupported API version: {}'.format(apiversion)) |
114 try: | 119 try: |
115 yield self._validate_executor.submit( | 120 yield self._validate_executor.submit( |
116 blocking.validate, self._apiurl, user.password, bundle) | 121 blocking.validate, self._apiurl, user.password, bundle) |
117 except Exception as err: | 122 except Exception as err: |
118 raise gen.Return(str(err)) | 123 raise gen.Return(str(err)) |
119 | 124 |
120 def import_bundle(self, user, name, bundle, test_callback=None): | 125 def import_bundle(self, user, name, bundle, bundle_id, test_callback=None): |
frankban
2013/11/15 08:48:20
It would be nice to make bundle_id explicitly opti
bac
2013/11/15 12:45:10
I don't see the reason. It is optional in the par
| |
121 """Schedule a deployment bundle import process. | 126 """Schedule a deployment bundle import process. |
122 | 127 |
123 The deployment is executed in a separate process. | 128 The deployment is executed in a separate process. |
124 | 129 |
125 Three arguments are required: | 130 The following arguments are required: |
126 - user: the current authenticated user; | 131 - user: the current authenticated user; |
127 - name: then name of the bundle to be imported; | 132 - name: the name of the bundle to be imported; |
128 - bundle: a YAML decoded object representing the bundle contents. | 133 - bundle: a YAML decoded object representing the bundle contents. |
134 - bundle_id: the ID of the bundle. May be None. | |
129 | 135 |
130 It is possible to also provide an optional test_callback that will be | 136 It is possible to also provide an optional test_callback that will be |
131 called when the deployment is completed. Note that this functionality | 137 called when the deployment is completed. Note that this functionality |
132 is present only for tests: clients should not consider the | 138 is present only for tests: clients should not consider the |
133 test_callback argument part of the API, and should instead use the | 139 test_callback argument part of the API, and should instead use the |
134 watch/next methods to observe the progress of a deployment (see below). | 140 watch/next methods to observe the progress of a deployment (see below). |
135 | 141 |
136 Return the deployment identifier assigned to this deployment process. | 142 Return the deployment identifier assigned to this deployment process. |
137 """ | 143 """ |
138 # Start observing this deployment, retrieve the next available | 144 # Start observing this deployment, retrieve the next available |
139 # deployment id and notify its position at the end of the queue. | 145 # deployment id and notify its position at the end of the queue. |
140 deployment_id = self._observer.add_deployment() | 146 deployment_id = self._observer.add_deployment() |
141 self._observer.notify_position(deployment_id, len(self._queue)) | 147 self._observer.notify_position(deployment_id, len(self._queue)) |
142 # Add this deployment to the queue. | 148 # Add this deployment to the queue. |
143 self._queue.append(deployment_id) | 149 self._queue.append(deployment_id) |
144 # Add the import bundle job to the run executor, and set up a callback | 150 # Add the import bundle job to the run executor, and set up a callback |
145 # to be called when the import process completes. | 151 # to be called when the import process completes. |
146 future = self._run_executor.submit( | 152 future = self._run_executor.submit( |
147 blocking.import_bundle, | 153 blocking.import_bundle, |
148 self._apiurl, user.password, name, bundle, IMPORTER_OPTIONS) | 154 self._apiurl, user.password, name, bundle, IMPORTER_OPTIONS) |
149 add_future(self._io_loop, future, self._import_callback, deployment_id) | 155 add_future(self._io_loop, future, self._import_callback, |
156 deployment_id, bundle_id) | |
150 self._futures[deployment_id] = future | 157 self._futures[deployment_id] = future |
158 if bundle_id is not None: | |
159 self.bundle_ids[deployment_id] = bundle_id | |
frankban
2013/11/15 08:48:20
AFAICT these two lines can be removed as well.
bac
2013/11/15 12:45:10
Done.
| |
151 # If a customized callback is provided, schedule it as well. | 160 # If a customized callback is provided, schedule it as well. |
152 if test_callback is not None: | 161 if test_callback is not None: |
153 add_future(self._io_loop, future, test_callback) | 162 add_future(self._io_loop, future, test_callback) |
154 # Submit a sleeping job in order to avoid the next deployment job to be | 163 # Submit a sleeping job in order to avoid the next deployment job to be |
155 # immediately put in the executor's call queue. This allows for | 164 # immediately put in the executor's call queue. This allows for |
156 # cancelling scheduled jobs, even if the job is the next to be started. | 165 # cancelling scheduled jobs, even if the job is the next to be started. |
157 self._run_executor.submit(time.sleep, 1) | 166 self._run_executor.submit(time.sleep, 1) |
158 return deployment_id | 167 return deployment_id |
159 | 168 |
160 def _import_callback(self, deployment_id, future): | 169 def _import_callback(self, deployment_id, bundle_id, future): |
161 """Callback called when a deployment process is completed. | 170 """Callback called when a deployment process is completed. |
162 | 171 |
163 This callback, scheduled in self.import_bundle(), receives the | 172 This callback, scheduled in self.import_bundle(), receives the |
164 deployment_id identifying one specific deployment job, and the fired | 173 deployment_id identifying one specific deployment job, and the fired |
165 future returned by the executor. | 174 future returned by the executor. |
166 """ | 175 """ |
167 if future.cancelled(): | 176 if future.cancelled(): |
168 # Notify a deployment has been cancelled. | 177 # Notify a deployment has been cancelled. |
169 self._observer.notify_cancelled(deployment_id) | 178 self._observer.notify_cancelled(deployment_id) |
170 else: | 179 else: |
171 exception = future.exception() | 180 exception = future.exception() |
172 error = None if exception is None else str(exception) | 181 error = None if exception is None else str(exception) |
173 # Notify a deployment completed. | 182 # Notify a deployment completed. |
174 self._observer.notify_completed(deployment_id, error=error) | 183 self._observer.notify_completed(deployment_id, error=error) |
175 # Remove the completed deployment job from the queue. | 184 # Remove the completed deployment job from the queue. |
176 self._queue.remove(deployment_id) | 185 self._queue.remove(deployment_id) |
177 del self._futures[deployment_id] | 186 del self._futures[deployment_id] |
178 # Notify the new position of all remaining deployments in the queue. | 187 # Notify the new position of all remaining deployments in the queue. |
179 for position, deploy_id in enumerate(self._queue): | 188 for position, deploy_id in enumerate(self._queue): |
180 self._observer.notify_position(deploy_id, position) | 189 self._observer.notify_position(deploy_id, position) |
190 # Increment the Charmworld deployment count upon successful | |
frankban
2013/11/15 08:48:20
The count is increased also if the deployment fail
bac
2013/11/15 12:45:10
I'd really like to only increment on success. I'l
| |
191 # deployment. | |
192 if bundle_id is not None: | |
frankban
2013/11/15 08:48:20
Nice and simple.
| |
193 utils.increment_deployment_counter( | |
194 bundle_id, self.charmworldurl) | |
181 | 195 |
182 def watch(self, deployment_id): | 196 def watch(self, deployment_id): |
183 """Start watching a deployment and return a watcher identifier. | 197 """Start watching a deployment and return a watcher identifier. |
184 | 198 |
185 The watcher id can be used by clients to observe changes occurring | 199 The watcher id can be used by clients to observe changes occurring |
186 during the deployment process identified by the deployment id. | 200 during the deployment process identified by the deployment id. |
187 Use the returned watcher id to start observing deployment changes | 201 Use the returned watcher id to start observing deployment changes |
188 (see the self.next() method below). | 202 (see the self.next() method below). |
189 | 203 |
190 Return None if the deployment identifier is not valid. | 204 Return None if the deployment identifier is not valid. |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
225 """ | 239 """ |
226 watchers = self._observer.deployments.values() | 240 watchers = self._observer.deployments.values() |
227 return [i.getlast() for i in watchers] | 241 return [i.getlast() for i in watchers] |
228 | 242 |
229 | 243 |
230 class DeployMiddleware(object): | 244 class DeployMiddleware(object): |
231 """Handle the bundles deployment request/response process. | 245 """Handle the bundles deployment request/response process. |
232 | 246 |
233 This class handles the process of parsing requests from the GUI, checking | 247 This class handles the process of parsing requests from the GUI, checking |
234 if any incoming message is a deployment request, ensuring that the request | 248 if any incoming message is a deployment request, ensuring that the request |
235 is well formed and, if so, forwarding the requests to the bundle views. | 249 is well-formed and, if so, forwarding the requests to the bundle views. |
236 | 250 |
237 Assuming that: | 251 Assuming that: |
238 - user is a guiserver.auth.User instance (used by this middleware in | 252 - user is a guiserver.auth.User instance (used by this middleware in |
239 order to retrieve the credentials for connecting the Deployer to the | 253 order to retrieve the credentials for connecting the Deployer to the |
240 Juju API server); | 254 Juju API server); |
241 - deployer is a guiserver.bundles.base.Deployer instance; | 255 - deployer is a guiserver.bundles.base.Deployer instance; |
242 - write_response is a callable that will be used to send responses to the | 256 - write_response is a callable that will be used to send responses to the |
243 client, i.e. deployments status and the results; | 257 client, i.e. deployments status and the results; |
244 - data is a JSON decoded object representing a single Juju API request; | 258 - data is a JSON decoded object representing a single Juju API request; |
245 here is an usage example: | 259 here is an usage example: |
(...skipping 26 matching lines...) Expand all Loading... | |
272 | 286 |
273 @gen.coroutine | 287 @gen.coroutine |
274 def process_request(self, data): | 288 def process_request(self, data): |
275 """Process a deployment request.""" | 289 """Process a deployment request.""" |
276 request_id = data['RequestId'] | 290 request_id = data['RequestId'] |
277 view = self.routes[data['Request']] | 291 view = self.routes[data['Request']] |
278 request = ObjectDict(params=data.get('Params', {}), user=self._user) | 292 request = ObjectDict(params=data.get('Params', {}), user=self._user) |
279 response = yield view(request, self._deployer) | 293 response = yield view(request, self._deployer) |
280 response['RequestId'] = request_id | 294 response['RequestId'] = request_id |
281 self._write_response(response) | 295 self._write_response(response) |
OLD | NEW |