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 add_relation(self
, a
, b
, via
=None):
180 Add a relation between two application endpoints.
182 :param a An application endpoint
183 :param b An application endpoint
184 :param via The egress subnet(s) for outbound traffic, e.g., (192.168.0.0/16,10.0.0.0/8)
186 if not self
.authenticated
:
189 m
= await self
.get_model()
191 m
.add_relation(a
, b
, via
)
195 async def apply_config(self
, config
, application
):
196 """Apply a configuration to the application."""
197 self
.log
.debug("JujuApi: Applying configuration to {}.".format(
200 return await self
.set_config(application
=application
, config
=config
)
202 async def deploy_application(self
, charm
, name
="", path
="", specs
={}):
204 Deploy an application.
206 Deploy an application to a container or a machine already provisioned
207 by the OSM Resource Orchestrator (requires the Juju public ssh key
208 installed on the new machine via cloud-init).
210 :param str charm: The name of the charm
211 :param str name: The name of the application, if different than the charm
212 :param str path: The path to the charm
213 :param dict machine: A dictionary identifying the machine to manage via Juju
216 deploy_application(..., specs={'host': '10.0.0.4', 'user': 'ubuntu'})
218 if not self
.authenticated
:
221 # Check that the charm is valid and exists.
225 app
= await self
.get_application(name
)
228 # Check for specific machine placement
230 if all(k
in specs
for k
in ['hostname', 'username']):
231 machine
= await self
.model
.add_machine(spec
='ssh:%@%'.format(
237 # TODO: Handle the error if the charm isn't found.
238 self
.log
.debug("JujuApi: Deploying charm {} ({}) from {}".format(
244 app
= await self
.model
.deploy(
246 application_name
=name
,
250 deploy_service
= deploy_application
252 async def get_action_status(self
, uuid
):
253 """Get the status of an action."""
254 if not self
.authenticated
:
257 self
.log
.debug("JujuApi: Waiting for status of action uuid {}".format(uuid
))
258 action
= await self
.model
.wait_for_action(uuid
)
261 async def get_application(self
, application
):
262 """Get the deployed application."""
263 if not self
.authenticated
:
266 self
.log
.debug("JujuApi: Getting application {}".format(application
))
268 if application
and self
.model
:
269 if self
.model
.applications
:
270 if application
in self
.model
.applications
:
271 app
= self
.model
.applications
[application
]
274 async def get_application_status(self
, application
):
275 """Get the status of an application."""
276 if not self
.authenticated
:
280 app
= await self
.get_application(application
)
283 self
.log
.debug("JujuApi: Status of application {} is {}".format(
288 get_service_status
= get_application_status
290 async def get_config(self
, application
):
291 """Get the configuration of an application."""
292 if not self
.authenticated
:
296 app
= await self
.get_application(application
)
298 config
= await app
.get_config()
300 self
.log
.debug("JujuApi: Config of application {} is {}".format(
307 async def get_model(self
, name
='default'):
308 """Get a model from the Juju Controller.
310 Note: Model objects returned must call disconnected() before it goes
312 if not self
.authenticated
:
317 uuid
= await self
.get_model_uuid(name
)
319 self
.log
.debug("JujuApi: Connecting to model {} ({})".format(
334 async def get_model_uuid(self
, name
='default'):
335 """Get the UUID of a model.
337 Iterate through all models in a controller and find the matching model.
339 if not self
.authenticated
:
344 models
= await self
.controller
.get_models()
346 self
.log
.debug("JujuApi: Looking through {} models for model {}".format(
347 len(models
.user_models
),
350 for model
in models
.user_models
:
351 if model
.model
.name
== name
:
352 uuid
= model
.model
.uuid
357 async def get_status(self
):
358 """Get the model status."""
359 if not self
.authenticated
:
363 self
.model
= self
.get_model(self
.model_name
)
370 self
.log
.debug("JujuApi: Getting model status")
371 status
= model_state()
372 status
.applications
= self
.model
.applications
373 status
.machines
= self
.model
.machines
377 async def is_application_active(self
, application
):
378 """Check if the application is in an active state."""
379 if not self
.authenticated
:
383 status
= await self
.get_application_status(application
)
384 if status
and status
in ['active']:
387 self
.log
.debug("JujuApi: Application {} is {} active".format(
389 "" if status
else "not",
393 is_service_active
= is_application_active
395 async def is_application_blocked(self
, application
):
396 """Check if the application is in a blocked state."""
397 if not self
.authenticated
:
401 status
= await self
.get_application_status(application
)
402 if status
and status
in ['blocked']:
405 self
.log
.debug("JujuApi: Application {} is {} blocked".format(
407 "" if status
else "not",
411 is_service_blocked
= is_application_blocked
413 async def is_application_deployed(self
, application
):
414 """Check if the application is in a deployed state."""
415 if not self
.authenticated
:
419 status
= await self
.get_application_status(application
)
420 if status
and status
in ['active']:
422 self
.log
.debug("JujuApi: Application {} is {} deployed".format(
424 "" if status
else "not",
428 is_service_deployed
= is_application_deployed
430 async def is_application_error(self
, application
):
431 """Check if the application is in an error state."""
432 if not self
.authenticated
:
436 status
= await self
.get_application_status(application
)
437 if status
and status
in ['error']:
439 self
.log
.debug("JujuApi: Application {} is {} errored".format(
441 "" if status
else "not",
445 is_service_error
= is_application_error
447 async def is_application_maint(self
, application
):
448 """Check if the application is in a maintenance state."""
449 if not self
.authenticated
:
453 status
= await self
.get_application_status(application
)
454 if status
and status
in ['maintenance']:
456 self
.log
.debug("JujuApi: Application {} is {} in maintenence".format(
458 "" if status
else "not",
462 is_service_maint
= is_application_maint
464 async def is_application_up(self
, application
=None):
465 """Check if the application is up."""
466 if not self
.authenticated
:
470 status
= await self
.get_application_status(application
)
471 if status
and status
in ['active', 'blocked']:
473 self
.log
.debug("JujuApi: Application {} is {} up".format(
475 "" if status
else "not",
478 is_service_up
= is_application_up
480 async def login(self
):
481 """Login to the Juju controller."""
482 if self
.authenticated
:
485 self
.controller
= Controller()
487 self
.log
.debug("JujuApi: Logging into controller")
490 await self
.controller
.connect(
497 await self
.controller
.connect_current()
499 self
.authenticated
= True
500 self
.model
= await self
.get_model(self
.model_name
)
502 async def logout(self
):
503 """Logout of the Juju controller."""
504 if not self
.authenticated
:
508 await self
.model
.disconnect()
511 await self
.controller
.disconnect()
512 self
.controller
= None
514 self
.authenticated
= False
516 async def remove_application(self
, name
):
517 """Remove the application."""
518 if not self
.authenticated
:
521 app
= await self
.get_application(name
)
523 self
.log
.debug("JujuApi: Destroying application {}".format(
529 async def remove_relation(self
, a
, b
):
531 Remove a relation between two application endpoints
533 :param a An application endpoint
534 :param b An application endpoint
536 if not self
.authenticated
:
539 m
= await self
.get_model()
541 m
.remove_relation(a
, b
)
545 async def resolve_error(self
, application
=None):
546 """Resolve units in error state."""
547 if not self
.authenticated
:
550 app
= await self
.get_application(application
)
552 self
.log
.debug("JujuApi: Resolving errors for application {}".format(
556 for unit
in app
.units
:
557 app
.resolved(retry
=True)
559 async def run_action(self
, application
, action_name
, **params
):
560 """Execute an action and return an Action object."""
561 if not self
.authenticated
:
570 app
= await self
.get_application(application
)
572 # We currently only have one unit per application
573 # so use the first unit available.
576 self
.log
.debug("JujuApi: Running Action {} against Application {}".format(
581 action
= await unit
.run_action(action_name
, **params
)
583 # Wait for the action to complete
586 result
['status'] = action
.status
587 result
['action']['tag'] = action
.data
['id']
588 result
['action']['results'] = action
.results
591 execute_action
= run_action
593 async def set_config(self
, application
, config
):
594 """Apply a configuration to the application."""
595 if not self
.authenticated
:
598 app
= await self
.get_application(application
)
600 self
.log
.debug("JujuApi: Setting config for Application {}".format(
603 await app
.set_config(config
)
605 # Verify the config is set
606 newconf
= await app
.get_config()
608 if config
[key
] != newconf
[key
]:
609 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
612 async def set_parameter(self
, parameter
, value
, application
=None):
613 """Set a config parameter for a service."""
614 if not self
.authenticated
:
617 self
.log
.debug("JujuApi: Setting {}={} for Application {}".format(
622 return await self
.apply_config(
624 application
=application
,
627 async def wait_for_application(self
, name
, timeout
=300):
628 """Wait for an application to become active."""
629 if not self
.authenticated
:
632 app
= await self
.get_application(name
)
634 self
.log
.debug("JujuApi: Waiting {} seconds for Application {}".format(
639 await self
.model
.block_until(
641 unit
.agent_status
== 'idle'
642 and unit
.workload_status
643 in ['active', 'unknown'] for unit
in app
.units
650 parser
= argparse
.ArgumentParser(description
='Test Driver for Juju API')
657 default
='10.0.202.49',
658 help="Juju controller",
663 help="User, default user-admin"
668 help="Password for the user"
673 help="Port number, default 17070"
678 help="The model to connect to."
686 help="Local directory for the charm"
698 To deploy to a non-Juju machine, provide the host and
699 credentials for Juju to manually provision (host, username, (password or key?))
705 help="Deploy as a proxy charm.",
709 action
='store_false',
711 help="Deploy as a full charm.",
713 parser
.set_defaults(proxy
=True)
721 return parser
.parse_args()
724 async def deploy_charm_and_wait():
725 args
= get_argparser()
727 # Set logging level to debug so we can see verbose output from the
729 logging
.basicConfig(level
=logging
.DEBUG
)
731 # Quiet logging from the websocket library. If you want to see
732 # everything sent over the wire, set this to DEBUG.
733 ws_logger
= logging
.getLogger('websockets.protocol')
734 ws_logger
.setLevel(logging
.INFO
)
736 """Here's an example of a coroutine that will deploy a charm and wait until
737 it's ready to be used."""
738 api
= JujuApi(server
=args
.server
,
741 secret
=args
.password
,
744 model_name
=args
.model
746 print("Logging in...")
749 if api
.authenticated
:
750 status
= await api
.get_status()
751 print('Applications:', list(status
.applications
.keys()))
752 print('Machines:', list(status
.machines
.keys()))
754 if args
.directory
and args
.application
:
759 charm
= os
.path
.basename(
761 os
.path
.dirname(args
.directory
)
764 await api
.deploy_application(charm
,
765 name
=args
.application
,
769 # Wait for the application to fully deploy. This will block until the
770 # agent is in an idle state, and the charm's workload is either
771 # 'active' or 'unknown', meaning it's ready but the author did not
772 # explicitly set a workload state.
773 print("Waiting for application '{}' to deploy...".format(charm
))
775 # Deploy the charm and wait, periodically checking its status
776 await api
.wait_for_application(charm
, 30)
778 error
= await api
.is_application_error(charm
)
780 print("This application is in an error state.")
783 blocked
= await api
.is_application_blocked(charm
)
785 print("This application is blocked.")
788 # An extra check to see if the charm is ready
789 up
= await api
.is_application_up(charm
)
790 print("Application is {}".format("up" if up
else "down"))
792 print("Service {} is deployed".format(args
.application
))
794 ###################################
795 # Execute config on a proxy charm #
796 ###################################
797 config
= await api
.get_config(args
.application
)
798 hostname
= config
['ssh-username']['value']
799 rhostname
= hostname
[::-1]
801 # Apply the configuration
802 await api
.apply_config(
803 {'ssh-username': rhostname
}, application
=args
.application
806 # Get the configuration
807 config
= await api
.get_config(args
.application
)
809 # Verify the configuration has been updated
810 assert(config
['ssh-username']['value'] == rhostname
)
812 ####################################
813 # Get the status of an application #
814 ####################################
815 status
= await api
.get_application_status(charm
)
816 print("Application Status: {}".format(status
))
818 ###########################
819 # Execute a simple action #
820 ###########################
821 result
= await api
.run_action(charm
, 'get-ssh-public-key')
822 print("Action {} status is {} and returned {}".format(
824 result
['action']['tag'],
825 result
['action']['results']
828 #####################################
829 # Execute an action with parameters #
830 #####################################
831 result
= await api
.run_action(charm
, 'run', command
='hostname')
833 print("Action {} status is {} and returned {}".format(
835 result
['action']['tag'],
836 result
['action']['results']
839 print("Logging out...")
843 # get public key in juju controller? that can be pulled without need of a charm deployed and installed to vm via cloud-init
845 if __name__
== "__main__":
846 # Create a single event loop for running code asyncronously.
847 loop
= asyncio
.get_event_loop()
849 # An initial set of tasks to run
851 deploy_charm_and_wait(),
854 # TODO: optionally run forever and use a Watcher to monitor what's happening
855 loop
.run_until_complete(asyncio
.wait(tasks
))