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 # Check that the charm is valid and exists.
194 app
= await self
.get_application(name
)
196 # TODO: Handle the error if the charm isn't found.
197 self
.log
.debug("JujuApi: Deploying charm {} ({}) from {}".format(
202 app
= await self
.model
.deploy(
204 application_name
=name
,
208 deploy_service
= deploy_application
210 async def get_action_status(self
, uuid
):
211 """Get the status of an action."""
212 if not self
.authenticated
:
215 self
.log
.debug("JujuApi: Waiting for status of action uuid {}".format(uuid
))
216 action
= await self
.model
.wait_for_action(uuid
)
219 async def get_application(self
, application
):
220 """Get the deployed application."""
221 if not self
.authenticated
:
224 self
.log
.debug("JujuApi: Getting application {}".format(application
))
226 if application
and self
.model
:
227 if self
.model
.applications
:
228 if application
in self
.model
.applications
:
229 app
= self
.model
.applications
[application
]
232 async def get_application_status(self
, application
):
233 """Get the status of an application."""
234 if not self
.authenticated
:
238 app
= await self
.get_application(application
)
241 self
.log
.debug("JujuApi: Status of application {} is {}".format(
246 get_service_status
= get_application_status
248 async def get_config(self
, application
):
249 """Get the configuration of an application."""
250 if not self
.authenticated
:
254 app
= await self
.get_application(application
)
256 config
= await app
.get_config()
258 self
.log
.debug("JujuApi: Config of application {} is {}".format(
265 async def get_model(self
, name
='default'):
266 """Get a model from the Juju Controller.
268 Note: Model objects returned must call disconnected() before it goes
270 if not self
.authenticated
:
275 uuid
= await self
.get_model_uuid(name
)
277 self
.log
.debug("JujuApi: Connecting to model {} ({})".format(
292 async def get_model_uuid(self
, name
='default'):
293 """Get the UUID of a model.
295 Iterate through all models in a controller and find the matching model.
297 if not self
.authenticated
:
302 models
= await self
.controller
.get_models()
304 self
.log
.debug("JujuApi: Looking through {} models for model {}".format(
305 len(models
.user_models
),
308 for model
in models
.user_models
:
309 if model
.model
.name
== name
:
310 uuid
= model
.model
.uuid
315 async def get_status(self
):
316 """Get the model status."""
317 if not self
.authenticated
:
321 self
.model
= self
.get_model(self
.model_name
)
328 self
.log
.debug("JujuApi: Getting model status")
329 status
= model_state()
330 status
.applications
= self
.model
.applications
331 status
.machines
= self
.model
.machines
335 async def is_application_active(self
, application
):
336 """Check if the application is in an active state."""
337 if not self
.authenticated
:
341 status
= await self
.get_application_status(application
)
342 if status
and status
in ['active']:
345 self
.log
.debug("JujuApi: Application {} is {} active".format(
347 "" if status
else "not",
351 is_service_active
= is_application_active
353 async def is_application_blocked(self
, application
):
354 """Check if the application is in a blocked state."""
355 if not self
.authenticated
:
359 status
= await self
.get_application_status(application
)
360 if status
and status
in ['blocked']:
363 self
.log
.debug("JujuApi: Application {} is {} blocked".format(
365 "" if status
else "not",
369 is_service_blocked
= is_application_blocked
371 async def is_application_deployed(self
, application
):
372 """Check if the application is in a deployed state."""
373 if not self
.authenticated
:
377 status
= await self
.get_application_status(application
)
378 if status
and status
in ['active']:
380 self
.log
.debug("JujuApi: Application {} is {} deployed".format(
382 "" if status
else "not",
386 is_service_deployed
= is_application_deployed
388 async def is_application_error(self
, application
):
389 """Check if the application is in an error state."""
390 if not self
.authenticated
:
394 status
= await self
.get_application_status(application
)
395 if status
and status
in ['error']:
397 self
.log
.debug("JujuApi: Application {} is {} errored".format(
399 "" if status
else "not",
403 is_service_error
= is_application_error
405 async def is_application_maint(self
, application
):
406 """Check if the application is in a maintenance state."""
407 if not self
.authenticated
:
411 status
= await self
.get_application_status(application
)
412 if status
and status
in ['maintenance']:
414 self
.log
.debug("JujuApi: Application {} is {} in maintenence".format(
416 "" if status
else "not",
420 is_service_maint
= is_application_maint
422 async def is_application_up(self
, application
=None):
423 """Check if the application is up."""
424 if not self
.authenticated
:
428 status
= await self
.get_application_status(application
)
429 if status
and status
in ['active', 'blocked']:
431 self
.log
.debug("JujuApi: Application {} is {} up".format(
433 "" if status
else "not",
436 is_service_up
= is_application_up
438 async def login(self
):
439 """Login to the Juju controller."""
440 if self
.authenticated
:
443 self
.controller
= Controller()
445 self
.log
.debug("JujuApi: Logging into controller")
448 await self
.controller
.connect(
455 await self
.controller
.connect_current()
457 self
.authenticated
= True
458 self
.model
= await self
.get_model(self
.model_name
)
460 async def logout(self
):
461 """Logout of the Juju controller."""
462 if not self
.authenticated
:
466 await self
.model
.disconnect()
469 await self
.controller
.disconnect()
470 self
.controller
= None
472 self
.authenticated
= False
474 async def remove_application(self
, name
):
475 """Remove the application."""
476 if not self
.authenticated
:
479 app
= await self
.get_application(name
)
481 self
.log
.debug("JujuApi: Destroying application {}".format(
487 async def resolve_error(self
, application
=None):
488 """Resolve units in error state."""
489 if not self
.authenticated
:
492 app
= await self
.get_application(application
)
494 self
.log
.debug("JujuApi: Resolving errors for application {}".format(
498 for unit
in app
.units
:
499 app
.resolved(retry
=True)
501 async def run_action(self
, application
, action_name
, **params
):
502 """Execute an action and return an Action object."""
503 if not self
.authenticated
:
512 app
= await self
.get_application(application
)
514 # We currently only have one unit per application
515 # so use the first unit available.
518 self
.log
.debug("JujuApi: Running Action {} against Application {}".format(
523 action
= await unit
.run_action(action_name
, **params
)
525 # Wait for the action to complete
528 result
['status'] = action
.status
529 result
['action']['tag'] = action
.data
['id']
530 result
['action']['results'] = action
.results
533 execute_action
= run_action
535 async def set_config(self
, application
, config
):
536 """Apply a configuration to the application."""
537 if not self
.authenticated
:
540 app
= await self
.get_application(application
)
542 self
.log
.debug("JujuApi: Setting config for Application {}".format(
545 await app
.set_config(config
)
547 # Verify the config is set
548 newconf
= await app
.get_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
]))
554 async def set_parameter(self
, parameter
, value
, application
=None):
555 """Set a config parameter for a service."""
556 if not self
.authenticated
:
559 self
.log
.debug("JujuApi: Setting {}={} for Application {}".format(
564 return await self
.apply_config(
566 application
=application
,
569 async def wait_for_application(self
, name
, timeout
=300):
570 """Wait for an application to become active."""
571 if not self
.authenticated
:
574 app
= await self
.get_application(name
)
576 self
.log
.debug("JujuApi: Waiting {} seconds for Application {}".format(
581 await self
.model
.block_until(
583 unit
.agent_status
== 'idle'
584 and unit
.workload_status
585 in ['active', 'unknown'] for unit
in app
.units
592 parser
= argparse
.ArgumentParser(description
='Test Juju')
595 default
='10.0.202.49',
596 help="Juju controller"
601 help="User, default user-admin"
606 help="Password for the user"
611 help="Port number, default 17070"
615 help="Local directory for the charm"
623 help="IP of the VNF to configure"
628 help="The model to connect to."
630 return parser
.parse_args()
633 if __name__
== "__main__":
634 args
= get_argparser()
636 # Set logging level to debug so we can see verbose output from the
638 logging
.basicConfig(level
=logging
.DEBUG
)
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
)
645 endpoint
= '%s:%d' % (args
.server
, int(args
.port
))
647 loop
= asyncio
.get_event_loop()
649 api
= JujuApi(server
=args
.server
,
652 secret
=args
.password
,
655 model_name
=args
.model
658 juju
.loop
.run(api
.login())
660 status
= juju
.loop
.run(api
.get_status())
662 print('Applications:', list(status
.applications
.keys()))
663 print('Machines:', list(status
.machines
.keys()))
665 if args
.directory
and args
.application
:
667 charm
= os
.path
.basename(args
.directory
)
669 api
.deploy_application(charm
,
670 name
=args
.application
,
675 juju
.loop
.run(api
.wait_for_application(charm
))
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"))
681 print("Service {} is deployed".format(args
.application
))
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]
690 # Apply the configuration
691 juju
.loop
.run(api
.apply_config(
692 {'ssh-username': rhostname
}, application
=args
.application
695 # Get the configuration
696 config
= juju
.loop
.run(api
.get_config(args
.application
))
698 # Verify the configuration has been updated
699 assert(config
['ssh-username']['value'] == rhostname
)
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
))
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(
713 result
['action']['tag'],
714 result
['action']['results']
717 #####################################
718 # Execute an action with parameters #
719 #####################################
720 result
= juju
.loop
.run(
721 api
.run_action(charm
, 'run', command
='hostname')
723 print("Action {} status is {} and returned {}".format(
725 result
['action']['tag'],
726 result
['action']['results']
729 juju
.loop
.run(api
.logout())
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})
738 # while not api._is_service_active():
741 # print ("Service {} is in status {}".
742 # format(args.service, api._get_service_status()))
744 # res = api._execute_action('create-update-user', {'number': '125252352525',
745 # 'password': 'asfsaf'})
747 # print ("Action 'creat-update-user response: {}".format(res))
749 # status = res['status']
750 # while status not in [ 'completed', 'failed' ]:
752 # status = api._get_action_status(res['action']['tag'])['status']
754 # print("Action status: {}".format(status))
756 # # This action will fail as the number is non-numeric
757 # res = api._execute_action('delete-user', {'number': '125252352525asf'})
759 # print ("Action 'delete-user response: {}".format(res))
761 # status = res['status']
762 # while status not in [ 'completed', 'failed' ]:
764 # status = api._get_action_status(res['action']['tag'])['status']
766 # print("Action status: {}".format(status))