c4e3923bf15c8923e60856aa7358c36019726ad8
1 ############################################################################
2 # Copyright 2016 RIFT.io Inc #
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 #
8 # http://www.apache.org/licenses/LICENSE-2.0 #
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 ############################################################################
24 from juju
.controller
import Controller
25 from juju
.model
import Model
, ModelObserver
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/
35 class JujuVersionError(Exception):
39 class JujuApiError(Exception):
43 class JujuEnvError(JujuApiError
):
47 class JujuModelError(JujuApiError
):
51 class JujuStatusError(JujuApiError
):
55 class JujuUnitsError(JujuApiError
):
59 class JujuWaitUnitsError(JujuApiError
):
63 class JujuSrvNotDeployedError(JujuApiError
):
67 class JujuAddCharmError(JujuApiError
):
71 class JujuDeployError(JujuApiError
):
75 class JujuDestroyError(JujuApiError
):
79 class JujuResolveError(JujuApiError
):
83 class JujuActionError(JujuApiError
):
87 class JujuActionApiError(JujuActionError
):
91 class JujuActionInfoError(JujuActionError
):
95 class JujuActionExecError(JujuActionError
):
99 class JujuAuthenticationError(Exception):
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."""
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":
113 # # TODO: Add a hook when an action is complete
118 class JujuApi(object):
119 """JujuApi wrapper on jujuclient library.
121 There should be one instance of JujuApi for each VNF manged by Juju.
124 Currently we use one unit per service/VNF. So once a service
125 is deployed, we store the unit name and reuse it
134 authenticated
= False
144 model_name
='default',
146 """Initialize with the Juju credentials."""
151 self
.log
= logging
.getLogger(__name__
)
153 # Quiet websocket traffic
154 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
156 self
.log
.debug('JujuApi: instantiated')
162 if user
.startswith('user-'):
165 self
.user
= 'user-{}'.format(user
)
167 self
.endpoint
= '%s:%d' % (server
, int(port
))
169 self
.model_name
= model_name
175 """Close any open connections."""
178 async def apply_config(self
, config
, application
):
179 """Apply a configuration to the application."""
180 self
.log
.debug("JujuApi: Applying configuration to {}.".format(
183 return await self
.set_config(application
=application
, config
=config
)
185 async def deploy_application(self
, charm
, name
="", path
=""):
186 """Deploy an application."""
187 if not self
.authenticated
:
190 app
= await self
.get_application(name
)
192 # TODO: Handle the error if the charm isn't found.
193 self
.log
.debug("JujuApi: Deploying charm {} ({}) from {}".format(
198 app
= await self
.model
.deploy(
200 application_name
=name
,
204 deploy_service
= deploy_application
206 async def get_action_status(self
, uuid
):
207 """Get the status of an action."""
208 if not self
.authenticated
:
211 self
.log
.debug("JujuApi: Waiting for status of action uuid {}".format(uuid
))
212 action
= await self
.model
.wait_for_action(uuid
)
215 async def get_application(self
, application
):
216 """Get the deployed application."""
217 if not self
.authenticated
:
220 self
.log
.debug("JujuApi: Getting application {}".format(application
))
223 if self
.model
.applications
:
224 if application
in self
.model
.applications
:
225 app
= self
.model
.applications
[application
]
228 async def get_application_status(self
, application
):
229 """Get the status of an application."""
230 if not self
.authenticated
:
234 app
= await self
.get_application(application
)
238 self
.log
.debug("JujuApi: Status of application {} is {}".format(
243 get_service_status
= get_application_status
245 async def get_config(self
, application
):
246 """Get the configuration of an application."""
247 if not self
.authenticated
:
251 app
= await self
.get_application(application
)
253 config
= await app
.get_config()
255 self
.log
.debug("JujuApi: Config of application {} is {}".format(
262 async def get_model(self
, name
='default'):
263 """Get a model from the Juju Controller.
265 Note: Model objects returned must call disconnected() before it goes
267 if not self
.authenticated
:
272 uuid
= await self
.get_model_uuid(name
)
274 self
.log
.debug("JujuApi: Connecting to model {} ({})".format(
289 async def get_model_uuid(self
, name
='default'):
290 """Get the UUID of a model.
292 Iterate through all models in a controller and find the matching model.
294 if not self
.authenticated
:
299 models
= await self
.controller
.get_models()
301 self
.log
.debug("JujuApi: Looking through {} models for model {}".format(
302 len(models
.user_models
),
305 for model
in models
.user_models
:
306 if model
.model
.name
== name
:
307 uuid
= model
.model
.uuid
312 async def get_status(self
):
313 """Get the model status."""
314 if not self
.authenticated
:
318 self
.model
= self
.get_model(self
.model_name
)
325 self
.log
.debug("JujuApi: Getting model status")
326 status
= model_state()
327 status
.applications
= self
.model
.applications
328 status
.machines
= self
.model
.machines
332 async def is_application_active(self
, application
):
333 """Check if the application is in an active state."""
334 if not self
.authenticated
:
338 status
= await self
.get_application_status(application
)
339 if status
and status
in ['active']:
342 self
.log
.debug("JujuApi: Application {} is {} active".format(
344 "" if status
else "not",
348 is_service_active
= is_application_active
350 async def is_application_blocked(self
, application
):
351 """Check if the application is in a blocked state."""
352 if not self
.authenticated
:
356 status
= await self
.get_application_status(application
)
357 if status
and status
in ['blocked']:
360 self
.log
.debug("JujuApi: Application {} is {} blocked".format(
362 "" if status
else "not",
366 is_service_blocked
= is_application_blocked
368 async def is_application_deployed(self
, application
):
369 """Check if the application is in a deployed state."""
370 if not self
.authenticated
:
374 status
= await self
.get_application_status(application
)
375 if status
and status
in ['active']:
377 self
.log
.debug("JujuApi: Application {} is {} deployed".format(
379 "" if status
else "not",
383 is_service_deployed
= is_application_deployed
385 async def is_application_error(self
, application
):
386 """Check if the application is in an error state."""
387 if not self
.authenticated
:
391 status
= await self
.get_application_status(application
)
392 if status
and status
in ['error']:
394 self
.log
.debug("JujuApi: Application {} is {} errored".format(
396 "" if status
else "not",
400 is_service_error
= is_application_error
402 async def is_application_maint(self
, application
):
403 """Check if the application is in a maintenance state."""
404 if not self
.authenticated
:
408 status
= await self
.get_application_status(application
)
409 if status
and status
in ['maintenance']:
411 self
.log
.debug("JujuApi: Application {} is {} in maintenence".format(
413 "" if status
else "not",
417 is_service_maint
= is_application_maint
419 async def is_application_up(self
, application
=None):
420 """Check if the application is up."""
421 if not self
.authenticated
:
425 status
= await self
.get_application_status(application
)
426 if status
and status
in ['active', 'blocked']:
428 self
.log
.debug("JujuApi: Application {} is {} up".format(
430 "" if status
else "not",
433 is_service_up
= is_application_up
435 async def login(self
):
436 """Login to the Juju controller."""
437 if self
.authenticated
:
440 self
.controller
= Controller()
442 self
.log
.debug("JujuApi: Logging into controller")
445 await self
.controller
.connect(
452 await self
.controller
.connect_current()
454 self
.authenticated
= True
455 self
.model
= await self
.get_model(self
.model_name
)
457 async def logout(self
):
458 """Logout of the Juju controller."""
459 if not self
.authenticated
:
463 await self
.model
.disconnect()
466 await self
.controller
.disconnect()
467 self
.controller
= None
469 self
.authenticated
= False
471 async def remove_application(self
, name
):
472 """Remove the application."""
473 if not self
.authenticated
:
476 app
= await self
.get_application(name
)
478 self
.log
.debug("JujuApi: Destroying application {}".format(
484 async def resolve_error(self
, application
=None):
485 """Resolve units in error state."""
486 if not self
.authenticated
:
489 app
= await self
.get_application(application
)
491 self
.log
.debug("JujuApi: Resolving errors for application {}".format(
495 for unit
in app
.units
:
496 app
.resolved(retry
=True)
498 async def run_action(self
, application
, action_name
, **params
):
499 """Execute an action and return an Action object."""
500 if not self
.authenticated
:
509 app
= await self
.get_application(application
)
511 # We currently only have one unit per application
512 # so use the first unit available.
515 self
.log
.debug("JujuApi: Running Action {} against Application {}".format(
520 action
= await unit
.run_action(action_name
, **params
)
522 # Wait for the action to complete
525 result
['status'] = action
.status
526 result
['action']['tag'] = action
.data
['id']
527 result
['action']['results'] = action
.results
530 execute_action
= run_action
532 async def set_config(self
, application
, config
):
533 """Apply a configuration to the application."""
534 if not self
.authenticated
:
537 app
= await self
.get_application(application
)
539 self
.log
.debug("JujuApi: Setting config for Application {}".format(
542 await app
.set_config(config
)
544 # Verify the config is set
545 newconf
= await app
.get_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
]))
551 async def set_parameter(self
, parameter
, value
, application
=None):
552 """Set a config parameter for a service."""
553 if not self
.authenticated
:
556 self
.log
.debug("JujuApi: Setting {}={} for Application {}".format(
561 return await self
.apply_config(
563 application
=application
,
566 async def wait_for_application(self
, name
, timeout
=300):
567 """Wait for an application to become active."""
568 if not self
.authenticated
:
571 app
= await self
.get_application(name
)
573 self
.log
.debug("JujuApi: Waiting {} seconds for Application {}".format(
578 await self
.model
.block_until(
580 unit
.agent_status
== 'idle'
581 and unit
.workload_status
582 in ['active', 'unknown'] for unit
in app
.units
589 parser
= argparse
.ArgumentParser(description
='Test Juju')
592 default
='10.0.202.49',
593 help="Juju controller"
598 help="User, default user-admin"
603 help="Password for the user"
608 help="Port number, default 17070"
612 help="Local directory for the charm"
620 help="IP of the VNF to configure"
625 help="The model to connect to."
627 return parser
.parse_args()
630 if __name__
== "__main__":
631 args
= get_argparser()
633 # Set logging level to debug so we can see verbose output from the
635 logging
.basicConfig(level
=logging
.DEBUG
)
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
)
642 endpoint
= '%s:%d' % (args
.server
, int(args
.port
))
644 loop
= asyncio
.get_event_loop()
646 api
= JujuApi(server
=args
.server
,
649 secret
=args
.password
,
652 model_name
=args
.model
655 juju
.loop
.run(api
.login())
657 status
= juju
.loop
.run(api
.get_status())
659 print('Applications:', list(status
.applications
.keys()))
660 print('Machines:', list(status
.machines
.keys()))
662 if args
.directory
and args
.application
:
664 charm
= os
.path
.basename(args
.directory
)
666 api
.deploy_application(charm
,
667 name
=args
.application
,
672 juju
.loop
.run(api
.wait_for_application(charm
))
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"))
678 print("Service {} is deployed".format(args
.application
))
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]
687 # Apply the configuration
688 juju
.loop
.run(api
.apply_config(
689 {'ssh-username': rhostname
}, application
=args
.application
692 # Get the configuration
693 config
= juju
.loop
.run(api
.get_config(args
.application
))
695 # Verify the configuration has been updated
696 assert(config
['ssh-username']['value'] == rhostname
)
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
))
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(
710 result
['action']['tag'],
711 result
['action']['results']
714 #####################################
715 # Execute an action with parameters #
716 #####################################
717 result
= juju
.loop
.run(
718 api
.run_action(charm
, 'run', command
='hostname')
720 print("Action {} status is {} and returned {}".format(
722 result
['action']['tag'],
723 result
['action']['results']
726 juju
.loop
.run(api
.logout())
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})
735 # while not api._is_service_active():
738 # print ("Service {} is in status {}".
739 # format(args.service, api._get_service_status()))
741 # res = api._execute_action('create-update-user', {'number': '125252352525',
742 # 'password': 'asfsaf'})
744 # print ("Action 'creat-update-user response: {}".format(res))
746 # status = res['status']
747 # while status not in [ 'completed', 'failed' ]:
749 # status = api._get_action_status(res['action']['tag'])['status']
751 # print("Action status: {}".format(status))
753 # # This action will fail as the number is non-numeric
754 # res = api._execute_action('delete-user', {'number': '125252352525asf'})
756 # print ("Action 'delete-user response: {}".format(res))
758 # status = res['status']
759 # while status not in [ 'completed', 'failed' ]:
761 # status = api._get_action_status(res['action']['tag'])['status']
763 # print("Action status: {}".format(status))