a06c1e7eb205069447c27c153685ec35bd6fa1bd
[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 # Quiet websocket traffic
154 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
155
156 self.log.debug('JujuApi: instantiated')
157
158 self.server = server
159 self.port = port
160
161 self.secret = secret
162 if user.startswith('user-'):
163 self.user = user
164 else:
165 self.user = 'user-{}'.format(user)
166
167 self.endpoint = '%s:%d' % (server, int(port))
168
169 self.model_name = model_name
170
171 if loop:
172 self.loop = loop
173
174 def __del__(self):
175 """Close any open connections."""
176 yield self.logout()
177
178 async def apply_config(self, config, application):
179 """Apply a configuration to the application."""
180 self.log.debug("JujuApi: Applying configuration to {}.".format(
181 application
182 ))
183 return await self.set_config(application=application, config=config)
184
185 async def deploy_application(self, charm, name="", path=""):
186 """Deploy an application."""
187 if not self.authenticated:
188 await self.login()
189
190 # Check that the charm is valid and exists.
191 if charm is None:
192 return None
193
194 app = await self.get_application(name)
195 if app is None:
196 # TODO: Handle the error if the charm isn't found.
197 self.log.debug("JujuApi: Deploying charm {} ({}) from {}".format(
198 charm,
199 name,
200 path,
201 ))
202 app = await self.model.deploy(
203 path,
204 application_name=name,
205 series='xenial',
206 )
207 return app
208 deploy_service = deploy_application
209
210 async def get_action_status(self, uuid):
211 """Get the status of an action."""
212 if not self.authenticated:
213 await self.login()
214
215 self.log.debug("JujuApi: Waiting for status of action uuid {}".format(uuid))
216 action = await self.model.wait_for_action(uuid)
217 return action.status
218
219 async def get_application(self, application):
220 """Get the deployed application."""
221 if not self.authenticated:
222 await self.login()
223
224 self.log.debug("JujuApi: Getting application {}".format(application))
225 app = None
226 if application and self.model:
227 if self.model.applications:
228 if application in self.model.applications:
229 app = self.model.applications[application]
230 return app
231
232 async def get_application_status(self, application):
233 """Get the status of an application."""
234 if not self.authenticated:
235 await self.login()
236
237 status = None
238 app = await self.get_application(application)
239 if app:
240 status = app.status
241 self.log.debug("JujuApi: Status of application {} is {}".format(
242 application,
243 str(status),
244 ))
245 return status
246 get_service_status = get_application_status
247
248 async def get_config(self, application):
249 """Get the configuration of an application."""
250 if not self.authenticated:
251 await self.login()
252
253 config = None
254 app = await self.get_application(application)
255 if app:
256 config = await app.get_config()
257
258 self.log.debug("JujuApi: Config of application {} is {}".format(
259 application,
260 str(config),
261 ))
262
263 return config
264
265 async def get_model(self, name='default'):
266 """Get a model from the Juju Controller.
267
268 Note: Model objects returned must call disconnected() before it goes
269 out of scope."""
270 if not self.authenticated:
271 await self.login()
272
273 model = Model()
274
275 uuid = await self.get_model_uuid(name)
276
277 self.log.debug("JujuApi: Connecting to model {} ({})".format(
278 model,
279 uuid,
280 ))
281
282 await model.connect(
283 self.endpoint,
284 uuid,
285 self.user,
286 self.secret,
287 None,
288 )
289
290 return model
291
292 async def get_model_uuid(self, name='default'):
293 """Get the UUID of a model.
294
295 Iterate through all models in a controller and find the matching model.
296 """
297 if not self.authenticated:
298 await self.login()
299
300 uuid = None
301
302 models = await self.controller.get_models()
303
304 self.log.debug("JujuApi: Looking through {} models for model {}".format(
305 len(models.user_models),
306 name,
307 ))
308 for model in models.user_models:
309 if model.model.name == name:
310 uuid = model.model.uuid
311 break
312
313 return uuid
314
315 async def get_status(self):
316 """Get the model status."""
317 if not self.authenticated:
318 await self.login()
319
320 if not self.model:
321 self.model = self.get_model(self.model_name)
322
323 class model_state:
324 applications = {}
325 machines = {}
326 relations = {}
327
328 self.log.debug("JujuApi: Getting model status")
329 status = model_state()
330 status.applications = self.model.applications
331 status.machines = self.model.machines
332
333 return status
334
335 async def is_application_active(self, application):
336 """Check if the application is in an active state."""
337 if not self.authenticated:
338 await self.login()
339
340 state = False
341 status = await self.get_application_status(application)
342 if status and status in ['active']:
343 state = True
344
345 self.log.debug("JujuApi: Application {} is {} active".format(
346 application,
347 "" if status else "not",
348 ))
349
350 return state
351 is_service_active = is_application_active
352
353 async def is_application_blocked(self, application):
354 """Check if the application is in a blocked state."""
355 if not self.authenticated:
356 await self.login()
357
358 state = False
359 status = await self.get_application_status(application)
360 if status and status in ['blocked']:
361 state = True
362
363 self.log.debug("JujuApi: Application {} is {} blocked".format(
364 application,
365 "" if status else "not",
366 ))
367
368 return state
369 is_service_blocked = is_application_blocked
370
371 async def is_application_deployed(self, application):
372 """Check if the application is in a deployed state."""
373 if not self.authenticated:
374 await self.login()
375
376 state = False
377 status = await self.get_application_status(application)
378 if status and status in ['active']:
379 state = True
380 self.log.debug("JujuApi: Application {} is {} deployed".format(
381 application,
382 "" if status else "not",
383 ))
384
385 return state
386 is_service_deployed = is_application_deployed
387
388 async def is_application_error(self, application):
389 """Check if the application is in an error state."""
390 if not self.authenticated:
391 await self.login()
392
393 state = False
394 status = await self.get_application_status(application)
395 if status and status in ['error']:
396 state = True
397 self.log.debug("JujuApi: Application {} is {} errored".format(
398 application,
399 "" if status else "not",
400 ))
401
402 return state
403 is_service_error = is_application_error
404
405 async def is_application_maint(self, application):
406 """Check if the application is in a maintenance state."""
407 if not self.authenticated:
408 await self.login()
409
410 state = False
411 status = await self.get_application_status(application)
412 if status and status in ['maintenance']:
413 state = True
414 self.log.debug("JujuApi: Application {} is {} in maintenence".format(
415 application,
416 "" if status else "not",
417 ))
418
419 return state
420 is_service_maint = is_application_maint
421
422 async def is_application_up(self, application=None):
423 """Check if the application is up."""
424 if not self.authenticated:
425 await self.login()
426 state = False
427
428 status = await self.get_application_status(application)
429 if status and status in ['active', 'blocked']:
430 state = True
431 self.log.debug("JujuApi: Application {} is {} up".format(
432 application,
433 "" if status else "not",
434 ))
435 return state
436 is_service_up = is_application_up
437
438 async def login(self):
439 """Login to the Juju controller."""
440 if self.authenticated:
441 return
442 cacert = None
443 self.controller = Controller()
444
445 self.log.debug("JujuApi: Logging into controller")
446
447 if self.secret:
448 await self.controller.connect(
449 self.endpoint,
450 self.user,
451 self.secret,
452 cacert,
453 )
454 else:
455 await self.controller.connect_current()
456
457 self.authenticated = True
458 self.model = await self.get_model(self.model_name)
459
460 async def logout(self):
461 """Logout of the Juju controller."""
462 if not self.authenticated:
463 return
464
465 if self.model:
466 await self.model.disconnect()
467 self.model = None
468 if self.controller:
469 await self.controller.disconnect()
470 self.controller = None
471
472 self.authenticated = False
473
474 async def remove_application(self, name):
475 """Remove the application."""
476 if not self.authenticated:
477 await self.login()
478
479 app = await self.get_application(name)
480 if app:
481 self.log.debug("JujuApi: Destroying application {}".format(
482 name,
483 ))
484
485 await app.destroy()
486
487 async def resolve_error(self, application=None):
488 """Resolve units in error state."""
489 if not self.authenticated:
490 await self.login()
491
492 app = await self.get_application(application)
493 if app:
494 self.log.debug("JujuApi: Resolving errors for application {}".format(
495 application,
496 ))
497
498 for unit in app.units:
499 app.resolved(retry=True)
500
501 async def run_action(self, application, action_name, **params):
502 """Execute an action and return an Action object."""
503 if not self.authenticated:
504 await self.login()
505 result = {
506 'status': '',
507 'action': {
508 'tag': None,
509 'results': None,
510 }
511 }
512 app = await self.get_application(application)
513 if app:
514 # We currently only have one unit per application
515 # so use the first unit available.
516 unit = app.units[0]
517
518 self.log.debug("JujuApi: Running Action {} against Application {}".format(
519 action_name,
520 application,
521 ))
522
523 action = await unit.run_action(action_name, **params)
524
525 # Wait for the action to complete
526 await action.wait()
527
528 result['status'] = action.status
529 result['action']['tag'] = action.data['id']
530 result['action']['results'] = action.results
531
532 return result
533 execute_action = run_action
534
535 async def set_config(self, application, config):
536 """Apply a configuration to the application."""
537 if not self.authenticated:
538 await self.login()
539
540 app = await self.get_application(application)
541 if app:
542 self.log.debug("JujuApi: Setting config for Application {}".format(
543 application,
544 ))
545 await app.set_config(config)
546
547 # Verify the config is set
548 newconf = await app.get_config()
549 for key in config:
550 if config[key] != newconf[key]:
551 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
552
553
554 async def set_parameter(self, parameter, value, application=None):
555 """Set a config parameter for a service."""
556 if not self.authenticated:
557 await self.login()
558
559 self.log.debug("JujuApi: Setting {}={} for Application {}".format(
560 parameter,
561 value,
562 application,
563 ))
564 return await self.apply_config(
565 {parameter: value},
566 application=application,
567 )
568
569 async def wait_for_application(self, name, timeout=300):
570 """Wait for an application to become active."""
571 if not self.authenticated:
572 await self.login()
573
574 app = await self.get_application(name)
575 if app:
576 self.log.debug("JujuApi: Waiting {} seconds for Application {}".format(
577 timeout,
578 name,
579 ))
580
581 await self.model.block_until(
582 lambda: all(
583 unit.agent_status == 'idle'
584 and unit.workload_status
585 in ['active', 'unknown'] for unit in app.units
586 ),
587 timeout=timeout,
588 )
589
590
591 def get_argparser():
592 parser = argparse.ArgumentParser(description='Test Juju')
593 parser.add_argument(
594 "-s", "--server",
595 default='10.0.202.49',
596 help="Juju controller"
597 )
598 parser.add_argument(
599 "-u", "--user",
600 default='admin',
601 help="User, default user-admin"
602 )
603 parser.add_argument(
604 "-p", "--password",
605 default='',
606 help="Password for the user"
607 )
608 parser.add_argument(
609 "-P", "--port",
610 default=17070,
611 help="Port number, default 17070"
612 )
613 parser.add_argument(
614 "-d", "--directory",
615 help="Local directory for the charm"
616 )
617 parser.add_argument(
618 "--application",
619 help="Charm name"
620 )
621 parser.add_argument(
622 "--vnf-ip",
623 help="IP of the VNF to configure"
624 )
625 parser.add_argument(
626 "-m", "--model",
627 default='default',
628 help="The model to connect to."
629 )
630 return parser.parse_args()
631
632
633 if __name__ == "__main__":
634 args = get_argparser()
635
636 # Set logging level to debug so we can see verbose output from the
637 # juju library.
638 logging.basicConfig(level=logging.DEBUG)
639
640 # Quiet logging from the websocket library. If you want to see
641 # everything sent over the wire, set this to DEBUG.
642 ws_logger = logging.getLogger('websockets.protocol')
643 ws_logger.setLevel(logging.INFO)
644
645 endpoint = '%s:%d' % (args.server, int(args.port))
646
647 loop = asyncio.get_event_loop()
648
649 api = JujuApi(server=args.server,
650 port=args.port,
651 user=args.user,
652 secret=args.password,
653 loop=loop,
654 log=ws_logger,
655 model_name=args.model
656 )
657
658 juju.loop.run(api.login())
659
660 status = juju.loop.run(api.get_status())
661
662 print('Applications:', list(status.applications.keys()))
663 print('Machines:', list(status.machines.keys()))
664
665 if args.directory and args.application:
666 # Deploy the charm
667 charm = os.path.basename(args.directory)
668 juju.loop.run(
669 api.deploy_application(charm,
670 name=args.application,
671 path=args.directory,
672 )
673 )
674
675 juju.loop.run(api.wait_for_application(charm))
676
677 # Wait for the service to come up
678 up = juju.loop.run(api.is_application_up(charm))
679 print("Application is {}".format("up" if up else "down"))
680
681 print("Service {} is deployed".format(args.application))
682
683 ###########################
684 # Execute config on charm #
685 ###########################
686 config = juju.loop.run(api.get_config(args.application))
687 hostname = config['ssh-username']['value']
688 rhostname = hostname[::-1]
689
690 # Apply the configuration
691 juju.loop.run(api.apply_config(
692 {'ssh-username': rhostname}, application=args.application
693 ))
694
695 # Get the configuration
696 config = juju.loop.run(api.get_config(args.application))
697
698 # Verify the configuration has been updated
699 assert(config['ssh-username']['value'] == rhostname)
700
701 ####################################
702 # Get the status of an application #
703 ####################################
704 status = juju.loop.run(api.get_application_status(charm))
705 print("Application Status: {}".format(status))
706
707 ###########################
708 # Execute a simple action #
709 ###########################
710 result = juju.loop.run(api.run_action(charm, 'get-ssh-public-key'))
711 print("Action {} status is {} and returned {}".format(
712 result['status'],
713 result['action']['tag'],
714 result['action']['results']
715 ))
716
717 #####################################
718 # Execute an action with parameters #
719 #####################################
720 result = juju.loop.run(
721 api.run_action(charm, 'run', command='hostname')
722 )
723 print("Action {} status is {} and returned {}".format(
724 result['status'],
725 result['action']['tag'],
726 result['action']['results']
727 ))
728
729 juju.loop.run(api.logout())
730
731 loop.close()
732
733 # if args.vnf_ip and \
734 # ('clearwater-aio' in args.directory):
735 # # Execute config on charm
736 # api._apply_config({'proxied_ip': args.vnf_ip})
737 #
738 # while not api._is_service_active():
739 # time.sleep(10)
740 #
741 # print ("Service {} is in status {}".
742 # format(args.service, api._get_service_status()))
743 #
744 # res = api._execute_action('create-update-user', {'number': '125252352525',
745 # 'password': 'asfsaf'})
746 #
747 # print ("Action 'creat-update-user response: {}".format(res))
748 #
749 # status = res['status']
750 # while status not in [ 'completed', 'failed' ]:
751 # time.sleep(2)
752 # status = api._get_action_status(res['action']['tag'])['status']
753 #
754 # print("Action status: {}".format(status))
755 #
756 # # This action will fail as the number is non-numeric
757 # res = api._execute_action('delete-user', {'number': '125252352525asf'})
758 #
759 # print ("Action 'delete-user response: {}".format(res))
760 #
761 # status = res['status']
762 # while status not in [ 'completed', 'failed' ]:
763 # time.sleep(2)
764 # status = api._get_action_status(res['action']['tag'])['status']
765 #
766 # print("Action status: {}".format(status))