Refactor JujuApi to use libjuju asyncronous API
[osm/SO.git] / common / python / rift / mano / utils / juju_api.py
1 ############################################################################
2 # Copyright 2016 RIFT.io Inc #
3 # #
4 # Licensed under the Apache License, Version 2.0 (the "License"); #
5 # you may not use this file except in compliance with the License. #
6 # You may obtain a copy of the License at #
7 # #
8 # http://www.apache.org/licenses/LICENSE-2.0 #
9 # #
10 # Unless required by applicable law or agreed to in writing, software #
11 # distributed under the License is distributed on an "AS IS" BASIS, #
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
13 # See the License for the specific language governing permissions and #
14 # limitations under the License. #
15 ############################################################################
16
17 import argparse
18 import asyncio
19 import logging
20 import os
21 import ssl
22
23 import juju.loop
24 from juju.controller import Controller
25 from juju.model import Model, ModelObserver
26
27 try:
28 ssl._create_default_https_context = ssl._create_unverified_context
29 except AttributeError:
30 # Legacy Python doesn't verify by default (see pep-0476)
31 # https://www.python.org/dev/peps/pep-0476/
32 pass
33
34
35 class JujuVersionError(Exception):
36 pass
37
38
39 class JujuApiError(Exception):
40 pass
41
42
43 class JujuEnvError(JujuApiError):
44 pass
45
46
47 class JujuModelError(JujuApiError):
48 pass
49
50
51 class JujuStatusError(JujuApiError):
52 pass
53
54
55 class JujuUnitsError(JujuApiError):
56 pass
57
58
59 class JujuWaitUnitsError(JujuApiError):
60 pass
61
62
63 class JujuSrvNotDeployedError(JujuApiError):
64 pass
65
66
67 class JujuAddCharmError(JujuApiError):
68 pass
69
70
71 class JujuDeployError(JujuApiError):
72 pass
73
74
75 class JujuDestroyError(JujuApiError):
76 pass
77
78
79 class JujuResolveError(JujuApiError):
80 pass
81
82
83 class JujuActionError(JujuApiError):
84 pass
85
86
87 class JujuActionApiError(JujuActionError):
88 pass
89
90
91 class JujuActionInfoError(JujuActionError):
92 pass
93
94
95 class JujuActionExecError(JujuActionError):
96 pass
97
98
99 class JujuAuthenticationError(Exception):
100 pass
101
102
103 class JujuMonitor(ModelObserver):
104 """Monitor state changes within the Juju Model."""
105 # async def on_change(self, delta, old, new, model):
106 # """React to changes in the Juju model."""
107 #
108 # # TODO: Setup the hook to update the UI if the status of a unit changes
109 # # to be used when deploying a charm and waiting for it to be "ready"
110 # if delta.entity in ['application', 'unit'] and delta.type == "change":
111 # pass
112 #
113 # # TODO: Add a hook when an action is complete
114
115 pass
116
117
118 class JujuApi(object):
119 """JujuApi wrapper on jujuclient library.
120
121 There should be one instance of JujuApi for each VNF manged by Juju.
122
123 Assumption:
124 Currently we use one unit per service/VNF. So once a service
125 is deployed, we store the unit name and reuse it
126 """
127
128 log = None
129 controller = None
130 models = {}
131 model = None
132 model_name = None
133 model_uuid = None
134 authenticated = False
135
136 def __init__(self,
137 log=None,
138 loop=None,
139 server='127.0.0.1',
140 port=17070,
141 user='admin',
142 secret=None,
143 version=None,
144 model_name='default',
145 ):
146 """Initialize with the Juju credentials."""
147
148 if log:
149 self.log = log
150 else:
151 self.log = logging.getLogger(__name__)
152
153 self.log.debug('JujuApi instantiated')
154
155 self.server = server
156 self.port = port
157
158 self.secret = secret
159 if user.startswith('user-'):
160 self.user = user
161 else:
162 self.user = 'user-{}'.format(user)
163
164 self.endpoint = '%s:%d' % (server, int(port))
165
166 self.model_name = model_name
167
168 if loop:
169 self.loop = loop
170
171 def __del__(self):
172 """Close any open connections."""
173 yield self.logout()
174
175 async def apply_config(self, config, application):
176 """Apply a configuration to the application."""
177 return await self.set_config(application=application, config=config)
178
179 async def deploy_application(self, charm, name="", path=""):
180 """Deploy an application."""
181 if not self.authenticated:
182 await self.login()
183
184 app = await self.get_application(name)
185 if app is None:
186 # TODO: Handle the error if the charm isn't found.
187 app = await self.model.deploy(
188 path,
189 application_name=name,
190 series='xenial',
191 )
192 return app
193 deploy_service = deploy_application
194
195 async def get_action_status(self, uuid):
196 """Get the status of an action."""
197 if not self.authenticated:
198 await self.login()
199
200 action = await self.model.wait_for_action(uuid)
201 return action.status
202
203 async def get_application(self, application):
204 """Get the deployed application."""
205 if not self.authenticated:
206 await self.login()
207
208 app = None
209 if application in self.model.applications:
210 app = self.model.applications[application]
211 return app
212
213 async def get_application_status(self, application, status=None):
214 """Get the status of an application."""
215 if not self.authenticated:
216 await self.login()
217
218 status = None
219 app = await self.get_application(application)
220 if app:
221 status = app.status
222
223 return status
224 get_service_status = get_application_status
225
226 async def get_config(self, application):
227 """Get the configuration of an application."""
228 if not self.authenticated:
229 await self.login()
230
231 config = None
232 app = await self.get_application(application)
233 if app:
234 config = await app.get_config()
235
236 return config
237
238 async def get_model(self, name='default'):
239 """Get a model from the Juju Controller.
240
241 Note: Model objects returned must call disconnected() before it goes
242 out of scope."""
243 if not self.authenticated:
244 await self.login()
245
246 model = Model()
247
248 uuid = await self.get_model_uuid(name)
249
250 await model.connect(
251 self.endpoint,
252 uuid,
253 self.user,
254 self.secret,
255 None,
256 )
257
258 return model
259
260 async def get_model_uuid(self, name='default'):
261 """Get the UUID of a model.
262
263 Iterate through all models in a controller and find the matching model.
264 """
265 if not self.authenticated:
266 await self.login()
267
268 uuid = None
269
270 models = await self.controller.get_models()
271 for model in models.user_models:
272 if model.model.name == name:
273 uuid = model.model.uuid
274 break
275
276 return uuid
277
278 async def get_status(self):
279 """Get the model status."""
280 if not self.authenticated:
281 await self.login()
282
283 if not self.model:
284 self.model = self.get_model(self.model_name)
285
286 class model_state:
287 applications = {}
288 machines = {}
289 relations = {}
290
291 status = model_state()
292 status.applications = self.model.applications
293 status.machines = self.model.machines
294
295 return status
296
297 async def is_application_active(self, application):
298 """Check if the application is in an active state."""
299 if not self.authenticated:
300 await self.login()
301
302 state = False
303 status = await self.get_application_status(application)
304 if status and status in ['active']:
305 state = True
306 return state
307 is_service_active = is_application_active
308
309 async def is_application_blocked(self, application):
310 """Check if the application is in a blocked state."""
311 if not self.authenticated:
312 await self.login()
313
314 state = False
315 status = await self.get_application_status(application)
316 if status and status in ['blocked']:
317 state = True
318 return state
319 is_service_blocked = is_application_blocked
320
321 async def is_application_deployed(self, application):
322 """Check if the application is in a deployed state."""
323 if not self.authenticated:
324 await self.login()
325
326 state = False
327 status = await self.get_application_status(application)
328 if status and status in ['active']:
329 state = True
330 return state
331 is_service_deployed = is_application_deployed
332
333 async def is_application_error(self, application):
334 """Check if the application is in an error state."""
335 if not self.authenticated:
336 await self.login()
337
338 state = False
339 status = await self.get_application_status(application)
340 if status and status in ['error']:
341 state = True
342 return state
343 is_service_error = is_application_error
344
345 async def is_application_maint(self, application):
346 """Check if the application is in a maintenance state."""
347 if not self.authenticated:
348 await self.login()
349
350 state = False
351 status = await self.get_application_status(application)
352 if status and status in ['maintenance']:
353 state = True
354 return state
355 is_service_maint = is_application_maint
356
357 async def is_application_up(self, application=None):
358 """Check if the application is up."""
359 if not self.authenticated:
360 await self.login()
361 state = False
362
363 status = await self.get_application_status(application)
364 if status and status in ['active', 'blocked']:
365 state = True
366 return state
367 is_service_up = is_application_up
368
369 async def login(self):
370 """Login to the Juju controller."""
371 if self.authenticated:
372 return
373 cacert = None
374 self.controller = Controller()
375
376 if self.secret:
377 await self.controller.connect(
378 self.endpoint,
379 self.user,
380 self.secret,
381 cacert,
382 )
383 else:
384 await self.controller.connect_current()
385
386 self.authenticated = True
387 self.model = await self.get_model(self.model_name)
388
389 async def logout(self):
390 """Logout of the Juju controller."""
391 if not self.authenticated:
392 return
393
394 if self.model:
395 await self.model.disconnect()
396 self.model = None
397 if self.controller:
398 await self.controller.disconnect()
399 self.controller = None
400
401 self.authenticated = False
402
403 async def remove_application(self, name):
404 """Remove the application."""
405 if not self.authenticated:
406 await self.login()
407
408 app = await self.get_application(name)
409 if app:
410 await app.destroy()
411
412 async def resolve_error(self, application=None, status=None):
413 """Resolve units in error state."""
414 if not self.authenticated:
415 await self.login()
416
417 app = await self.get_application(application)
418 if app:
419 for unit in app.units:
420 app.resolved(retry=True)
421
422 async def run_action(self, application, action_name, **params):
423 """Execute an action and return an Action object."""
424 if not self.authenticated:
425 await self.login()
426 result = {
427 'status': '',
428 'action': {
429 'tag': None,
430 'results': None,
431 }
432 }
433 app = await self.get_application(application)
434 if app:
435 # We currently only have one unit per application
436 # so use the first unit available.
437 unit = app.units[0]
438
439 action = await unit.run_action(action_name, **params)
440
441 # Wait for the action to complete
442 await action.wait()
443
444 result['status'] = action.status
445 result['action']['tag'] = action.data['id']
446 result['action']['results'] = action.results
447
448 return result
449 execute_action = run_action
450
451 async def set_config(self, application, config):
452 """Apply a configuration to the application."""
453 if not self.authenticated:
454 await self.login()
455
456 app = await self.get_application(application)
457 if app:
458 await app.set_config(config)
459
460 async def set_parameter(self, parameter, value, application=None):
461 """Set a config parameter for a service."""
462 if not self.authenticated:
463 await self.login()
464
465 return await self.apply_config(
466 {parameter: value},
467 application=application,
468 )
469
470 async def wait_for_application(self, name, timeout=300):
471 """Wait for an application to become active."""
472 if not self.authenticated:
473 await self.login()
474
475 app = await self.get_application(name)
476 if app:
477 await self.model.block_until(
478 lambda: all(
479 unit.agent_status == 'idle'
480 and unit.workload_status
481 in ['active', 'unknown'] for unit in app.units
482 ),
483 timeout=timeout,
484 )
485
486
487 def get_argparser():
488 parser = argparse.ArgumentParser(description='Test Juju')
489 parser.add_argument(
490 "-s", "--server",
491 default='10.0.202.49',
492 help="Juju controller"
493 )
494 parser.add_argument(
495 "-u", "--user",
496 default='admin',
497 help="User, default user-admin"
498 )
499 parser.add_argument(
500 "-p", "--password",
501 default='',
502 help="Password for the user"
503 )
504 parser.add_argument(
505 "-P", "--port",
506 default=17070,
507 help="Port number, default 17070"
508 )
509 parser.add_argument(
510 "-d", "--directory",
511 help="Local directory for the charm"
512 )
513 parser.add_argument(
514 "--application",
515 help="Charm name"
516 )
517 parser.add_argument(
518 "--vnf-ip",
519 help="IP of the VNF to configure"
520 )
521 parser.add_argument(
522 "-m", "--model",
523 default='default',
524 help="The model to connect to."
525 )
526 return parser.parse_args()
527
528
529 if __name__ == "__main__":
530 args = get_argparser()
531
532 # Set logging level to debug so we can see verbose output from the
533 # juju library.
534 logging.basicConfig(level=logging.DEBUG)
535
536 # Quiet logging from the websocket library. If you want to see
537 # everything sent over the wire, set this to DEBUG.
538 ws_logger = logging.getLogger('websockets.protocol')
539 ws_logger.setLevel(logging.INFO)
540
541 endpoint = '%s:%d' % (args.server, int(args.port))
542
543 loop = asyncio.get_event_loop()
544
545 api = JujuApi(server=args.server,
546 port=args.port,
547 user=args.user,
548 secret=args.password,
549 loop=loop,
550 log=ws_logger,
551 model_name=args.model
552 )
553
554 juju.loop.run(api.login())
555
556 status = juju.loop.run(api.get_status())
557
558 print('Applications:', list(status.applications.keys()))
559 print('Machines:', list(status.machines.keys()))
560
561 if args.directory and args.application:
562 # Deploy the charm
563 charm = os.path.basename(args.directory)
564 juju.loop.run(
565 api.deploy_application(charm,
566 name=args.application,
567 path=args.directory,
568 )
569 )
570
571 juju.loop.run(api.wait_for_application(charm))
572
573 # Wait for the service to come up
574 up = juju.loop.run(api.is_application_up(charm))
575 print("Application is {}".format("up" if up else "down"))
576
577 print("Service {} is deployed".format(args.application))
578
579 ###########################
580 # Execute config on charm #
581 ###########################
582 config = juju.loop.run(api.get_config(args.application))
583 hostname = config['ssh-username']['value']
584 rhostname = hostname[::-1]
585
586 # Apply the configuration
587 juju.loop.run(api.apply_config(
588 {'ssh-username': rhostname}, application=args.application
589 ))
590
591 # Get the configuration
592 config = juju.loop.run(api.get_config(args.application))
593
594 # Verify the configuration has been updated
595 assert(config['ssh-username']['value'] == rhostname)
596
597 ####################################
598 # Get the status of an application #
599 ####################################
600 status = juju.loop.run(api.get_application_status(charm))
601 print("Application Status: {}".format(status))
602
603 ###########################
604 # Execute a simple action #
605 ###########################
606 result = juju.loop.run(api.run_action(charm, 'get-ssh-public-key'))
607 print("Action {} status is {} and returned {}".format(
608 result['status'],
609 result['action']['tag'],
610 result['action']['results']
611 ))
612
613 #####################################
614 # Execute an action with parameters #
615 #####################################
616 result = juju.loop.run(
617 api.run_action(charm, 'run', command='hostname')
618 )
619 print("Action {} status is {} and returned {}".format(
620 result['status'],
621 result['action']['tag'],
622 result['action']['results']
623 ))
624
625 juju.loop.run(api.logout())
626
627 loop.close()
628
629 # if args.vnf_ip and \
630 # ('clearwater-aio' in args.directory):
631 # # Execute config on charm
632 # api._apply_config({'proxied_ip': args.vnf_ip})
633 #
634 # while not api._is_service_active():
635 # time.sleep(10)
636 #
637 # print ("Service {} is in status {}".
638 # format(args.service, api._get_service_status()))
639 #
640 # res = api._execute_action('create-update-user', {'number': '125252352525',
641 # 'password': 'asfsaf'})
642 #
643 # print ("Action 'creat-update-user response: {}".format(res))
644 #
645 # status = res['status']
646 # while status not in [ 'completed', 'failed' ]:
647 # time.sleep(2)
648 # status = api._get_action_status(res['action']['tag'])['status']
649 #
650 # print("Action status: {}".format(status))
651 #
652 # # This action will fail as the number is non-numeric
653 # res = api._execute_action('delete-user', {'number': '125252352525asf'})
654 #
655 # print ("Action 'delete-user response: {}".format(res))
656 #
657 # status = res['status']
658 # while status not in [ 'completed', 'failed' ]:
659 # time.sleep(2)
660 # status = api._get_action_status(res['action']['tag'])['status']
661 #
662 # print("Action status: {}".format(status))