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