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